From 87ec61c1b40f9003b6b878a57b7880b2602290e8 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Fri, 12 Jul 2024 01:32:54 +0900 Subject: [PATCH 01/39] [deploy] merge to develop (#27) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index c55d078f..6790a8d8 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -19,7 +19,7 @@ pipeline { DOCKER_HUB_FULL_URL = 'https://' + DOCKER_HUB_URL DOCKER_HUB_DEV_CREDENTIAL_ID = 'DOCKER_HUB_DEV_CREDENTIALS' DOCKER_HUB_PROD_CREDENTIAL_ID = 'DOCKER_HUB_PROD_CREDENTIALS' - DOCKER_IMAGE_NAME = BRANCH_NAME.equals(PROD_BRANCH) ? 'donghoon0203/beat-Prod' : 'hoonyworld/beat-dev' + DOCKER_IMAGE_NAME = BRANCH_NAME.equals(PROD_BRANCH) ? 'donghoon0203/beat-prod' : 'hoonyworld/beat-dev' // SSH SSH_CREDENTIAL_ID = OPERATION_ENV.toUpperCase() + '_SSH' From 2f62a589bfa11468eb77c7aa4f27e0d35b7879b5 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:03:52 +0900 Subject: [PATCH 02/39] =?UTF-8?q?[feat]=20#28=20-=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EC=98=88=EB=A7=A4=20=EC=A1=B0=ED=9A=8C=20POST=20AP?= =?UTF-8?q?I=20=EA=B5=AC=ED=98=84=20(#29)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 --- build.gradle | 2 +- .../domain/booking/api/BookingController.java | 32 +++++++ .../booking/application/BookingService.java | 84 +++++++++++++++++++ .../dto/BookingRetrieveRequest.java | 12 +++ .../dto/BookingRetrieveResponse.java | 54 ++++++++++++ .../domain/booking/dao/BookingRepository.java | 16 ++++ .../beat/domain/booking/domain/Booking.java | 47 +++++++++-- .../booking/exception/BookingErrorCode.java | 18 ++++ .../booking/exception/BookingSuccessCode.java | 15 ++++ .../performance/domain/Performance.java | 62 ++++++++++++-- .../beat/domain/schedule/domain/Schedule.java | 31 ++++++- .../beat/global/common/dto/ErrorResponse.java | 4 +- .../global/common/dto/SuccessResponse.java | 2 +- .../handler/GlobalExceptionHandler.java | 13 ++- src/main/resources/application-dev.yml | 1 + src/main/resources/application-prod.yml | 7 +- 16 files changed, 373 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/beat/domain/booking/api/BookingController.java create mode 100644 src/main/java/com/beat/domain/booking/application/BookingService.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java create mode 100644 src/main/java/com/beat/domain/booking/dao/BookingRepository.java create mode 100644 src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java create mode 100644 src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java diff --git a/build.gradle b/build.gradle index d57e0f66..1b8f6063 100644 --- a/build.gradle +++ b/build.gradle @@ -25,7 +25,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-security' +// implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java new file mode 100644 index 00000000..a4d73327 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -0,0 +1,32 @@ +package com.beat.domain.booking.api; + +import com.beat.domain.booking.application.BookingService; +import com.beat.domain.booking.application.dto.BookingRetrieveRequest; +import com.beat.domain.booking.application.dto.BookingRetrieveResponse; +import com.beat.domain.booking.exception.BookingSuccessCode; +import com.beat.global.common.dto.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/bookings") +@RequiredArgsConstructor +public class BookingController { + + private final BookingService bookingService; + + @PostMapping("/guest/retrieve") + public ResponseEntity>> findGuestBookings( + @RequestBody BookingRetrieveRequest bookingRetrieveRequest) { + List response = bookingService.findGuestBookings(bookingRetrieveRequest); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(BookingSuccessCode.BOOKING_RETRIEVE_SUCCESS, response)); + } +} diff --git a/src/main/java/com/beat/domain/booking/application/BookingService.java b/src/main/java/com/beat/domain/booking/application/BookingService.java new file mode 100644 index 00000000..9780dd65 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/BookingService.java @@ -0,0 +1,84 @@ +package com.beat.domain.booking.application; + +import com.beat.domain.booking.application.dto.BookingRetrieveRequest; +import com.beat.domain.booking.application.dto.BookingRetrieveResponse; +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.booking.exception.BookingErrorCode; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.regex.Pattern; + +@Service +@RequiredArgsConstructor +public class BookingService { + + private final BookingRepository bookingRepository; + + public List findGuestBookings(BookingRetrieveRequest bookingRetrieveRequest) { + + validateRequest(bookingRetrieveRequest); + + List bookings = bookingRepository.findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthDate( + bookingRetrieveRequest.bookerName(), bookingRetrieveRequest.bookerPhoneNumber(), bookingRetrieveRequest.password(), bookingRetrieveRequest.birthDate()).orElseThrow( + () -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + + if (bookings.isEmpty()) { + throw new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND); + } + + return bookings.stream() + .map(this::toBookingResponse) + .toList(); + } + + private void validateRequest(BookingRetrieveRequest bookingRetrieveRequest) { + if (bookingRetrieveRequest.bookerName() == null || bookingRetrieveRequest.bookerPhoneNumber() == null || bookingRetrieveRequest.password() == null || bookingRetrieveRequest.birthDate() == null) { + throw new BadRequestException(BookingErrorCode.REQUIRED_DATA_MISSING); + } + + if (!Pattern.matches("^[a-zA-Z가-힣]+$", bookingRetrieveRequest.bookerName())) { // 예매자 이름은 알파벳, 한글 형식 + throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); + } + + if (!Pattern.matches("^\\d{3}-\\d{4}-\\d{4}$", bookingRetrieveRequest.bookerPhoneNumber())) { // 전화번호는 010-1234-5678 형식 + throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); + } + + if (!Pattern.matches("^\\d{4}$", bookingRetrieveRequest.password())) { // 비밀번호는 4자리 숫자 형식 + throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); + } + + if (!Pattern.matches("^\\d{6}$", bookingRetrieveRequest.birthDate())) { // 생년월일은 6자리 숫자 형식 + throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); + } + } + + private BookingRetrieveResponse toBookingResponse(Booking booking) { + Schedule schedule = booking.getSchedule(); + Performance performance = schedule.getPerformance(); + + return BookingRetrieveResponse.of( + booking.getBookingId(), + schedule.getScheduleId(), + performance.getPerformanceTitle(), + schedule.getPerformanceDate(), + performance.getPerformanceVenue(), + booking.getPurchaseTicketCount(), + schedule.getScheduleNumber().name(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + performance.getBankName().name(), + performance.getAccountNumber(), + schedule.getScheduleId().intValue(), + booking.isPaymentCompleted(), + booking.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java b/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java new file mode 100644 index 00000000..0a63e70b --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java @@ -0,0 +1,12 @@ +package com.beat.domain.booking.application.dto; + +public record BookingRetrieveRequest( + String bookerName, + String birthDate, + String bookerPhoneNumber, + String password +) { + public static BookingRetrieveRequest of(String bookerName, String birthDate, String bookerPhoneNumber, String password) { + return new BookingRetrieveRequest(bookerName, birthDate, bookerPhoneNumber, password); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java new file mode 100644 index 00000000..c155f178 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java @@ -0,0 +1,54 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record BookingRetrieveResponse( + Long bookingId, + Long scheduleId, + String performanceTitle, + LocalDateTime performanceDate, + String performanceVenue, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + String bankName, + String accountNumber, + int dueDate, + boolean isPaymentCompleted, + LocalDateTime createdAt +) { + public static BookingRetrieveResponse of( + Long bookingId, + Long scheduleId, + String performanceTitle, + LocalDateTime performanceDate, + String performanceVenue, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + String bankName, + String accountNumber, + int dueDate, + boolean isPaymentCompleted, + LocalDateTime createdAt + ) { + return new BookingRetrieveResponse( + bookingId, + scheduleId, + performanceTitle, + performanceDate, + performanceVenue, + purchaseTicketCount, + scheduleNumber, + bookerName, + bookerPhoneNumber, + bankName, + accountNumber, + dueDate, + isPaymentCompleted, + createdAt + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java new file mode 100644 index 00000000..f5e98889 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java @@ -0,0 +1,16 @@ +package com.beat.domain.booking.dao; + +import com.beat.domain.booking.domain.Booking; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; +import java.util.Optional; + +public interface BookingRepository extends JpaRepository { + Optional> findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthDate( + String bookerName, + String bookerPhoneNumber, + String password, + String birthDate + ); +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/domain/Booking.java b/src/main/java/com/beat/domain/booking/domain/Booking.java index 9a4273a6..ef1d2ce5 100644 --- a/src/main/java/com/beat/domain/booking/domain/Booking.java +++ b/src/main/java/com/beat/domain/booking/domain/Booking.java @@ -1,13 +1,21 @@ package com.beat.domain.booking.domain; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.user.domain.Users; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; @@ -36,14 +44,43 @@ public class Booking { private LocalDateTime createdAt = LocalDateTime.now(); @Column(nullable = true) - private LocalDateTime birthDate; + private String birthDate; @Column(nullable = true) private String password; - @Column(nullable = false) - private Long scheduleId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "schedule_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Schedule schedule; - @Column(nullable = false) - private Long userId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Users users; + + @Builder + public Booking(int purchaseTicketCount, String bookerName, String bookerPhoneNumber, boolean isPaymentCompleted, String birthDate, String password, Schedule schedule, Users users) { + this.purchaseTicketCount = purchaseTicketCount; + this.bookerName = bookerName; + this.bookerPhoneNumber = bookerPhoneNumber; + this.isPaymentCompleted = isPaymentCompleted; + this.birthDate = birthDate; + this.password = password; + this.schedule = schedule; + this.users = users; + } + + public static Booking create(int purchaseTicketCount, String bookerName, String bookerPhoneNumber, boolean isPaymentCompleted, String birthDate, String password, Schedule schedule, Users users) { + return Booking.builder() + .purchaseTicketCount(purchaseTicketCount) + .bookerName(bookerName) + .bookerPhoneNumber(bookerPhoneNumber) + .isPaymentCompleted(isPaymentCompleted) + .birthDate(birthDate) + .password(password) + .schedule(schedule) + .users(users) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java b/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java new file mode 100644 index 00000000..b1ca314e --- /dev/null +++ b/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java @@ -0,0 +1,18 @@ +package com.beat.domain.booking.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BookingErrorCode implements BaseErrorCode { + REQUIRED_DATA_MISSING(400, "필수 데이터가 누락되었습니다."), + INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), + INVALID_REQUEST_FORMAT(400, "잘못된 요청 형식입니다."), + NO_BOOKING_FOUND(404, "입력하신 정보와 일치하는 예매 내역이 없습니다. 확인 후 다시 조회해주세요.") + ; + + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java new file mode 100644 index 00000000..074c47c5 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -0,0 +1,15 @@ +package com.beat.domain.booking.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BookingSuccessCode implements BaseSuccessCode { + BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") + ; + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/performance/domain/Performance.java b/src/main/java/com/beat/domain/performance/domain/Performance.java index d0f9923e..b24554a3 100644 --- a/src/main/java/com/beat/domain/performance/domain/Performance.java +++ b/src/main/java/com/beat/domain/performance/domain/Performance.java @@ -1,16 +1,14 @@ package com.beat.domain.performance.domain; import com.beat.domain.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import com.beat.domain.user.domain.Users; +import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -65,6 +63,52 @@ public class Performance extends BaseTimeEntity { @Column(nullable = false) private int totalScheduleCount; - @Column(nullable = false) - private Long userId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Users users; + + @Builder + public Performance(String performanceTitle, Genre genre, int runningTime, String performanceDescription, String performanceAttentionNote, + BankName bankName, String accountNumber, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, + String performancePeriod, int ticketPrice, int totalScheduleCount, Users users) { + this.performanceTitle = performanceTitle; + this.genre = genre; + this.runningTime = runningTime; + this.performanceDescription = performanceDescription; + this.performanceAttentionNote = performanceAttentionNote; + this.bankName = bankName; + this.accountNumber = accountNumber; + this.posterImage = posterImage; + this.performanceTeamName = performanceTeamName; + this.performanceVenue = performanceVenue; + this.performanceContact = performanceContact; + this.performancePeriod = performancePeriod; + this.ticketPrice = ticketPrice; + this.totalScheduleCount = totalScheduleCount; + this.users = users; + } + + public static Performance create( + String performanceTitle, Genre genre, int runningTime, String performanceDescription, String performanceAttentionNote, + BankName bankName, String accountNumber, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, + String performancePeriod, int ticketPrice, int totalScheduleCount, Users users) { + return Performance.builder() + .performanceTitle(performanceTitle) + .genre(genre) + .runningTime(runningTime) + .performanceDescription(performanceDescription) + .performanceAttentionNote(performanceAttentionNote) + .bankName(bankName) + .accountNumber(accountNumber) + .posterImage(posterImage) + .performanceTeamName(performanceTeamName) + .performanceVenue(performanceVenue) + .performanceContact(performanceContact) + .performancePeriod(performancePeriod) + .ticketPrice(ticketPrice) + .totalScheduleCount(totalScheduleCount) + .users(users) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/domain/Schedule.java b/src/main/java/com/beat/domain/schedule/domain/Schedule.java index f894c6f0..2f26eca1 100644 --- a/src/main/java/com/beat/domain/schedule/domain/Schedule.java +++ b/src/main/java/com/beat/domain/schedule/domain/Schedule.java @@ -1,13 +1,18 @@ package com.beat.domain.schedule.domain; +import com.beat.domain.performance.domain.Performance; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -38,6 +43,28 @@ public class Schedule { @Column(nullable = false) private ScheduleNumber scheduleNumber; - @Column(nullable = false) - private Long performanceId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = false) + private Performance performance; + + @Builder + public Schedule(LocalDateTime performanceDate, int totalTicketCount, int soldTicketCount, boolean isBooking, ScheduleNumber scheduleNumber, Performance performance) { + this.performanceDate = performanceDate; + this.totalTicketCount = totalTicketCount; + this.soldTicketCount = soldTicketCount; + this.isBooking = isBooking; + this.scheduleNumber = scheduleNumber; + this.performance = performance; + } + + public static Schedule create(LocalDateTime performanceDate, int totalTicketCount, int soldTicketCount, boolean isBooking, ScheduleNumber scheduleNumber, Performance performance) { + return Schedule.builder() + .performanceDate(performanceDate) + .totalTicketCount(totalTicketCount) + .soldTicketCount(soldTicketCount) + .isBooking(isBooking) + .scheduleNumber(scheduleNumber) + .performance(performance) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/dto/ErrorResponse.java b/src/main/java/com/beat/global/common/dto/ErrorResponse.java index e425787e..aedc4a79 100644 --- a/src/main/java/com/beat/global/common/dto/ErrorResponse.java +++ b/src/main/java/com/beat/global/common/dto/ErrorResponse.java @@ -10,7 +10,7 @@ public static ErrorResponse of(final int status, final String message) { return new ErrorResponse(status, message); } - public static ErrorResponse of(final BaseErrorCode baseErrorCode) { + public static ErrorResponse from(final BaseErrorCode baseErrorCode) { return new ErrorResponse(baseErrorCode.getStatus(), baseErrorCode.getMessage()); } -} +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/dto/SuccessResponse.java b/src/main/java/com/beat/global/common/dto/SuccessResponse.java index 6bbba315..ccc83c7d 100644 --- a/src/main/java/com/beat/global/common/dto/SuccessResponse.java +++ b/src/main/java/com/beat/global/common/dto/SuccessResponse.java @@ -11,7 +11,7 @@ public static SuccessResponse of(final BaseSuccessCode baseSuccessCode, fina return new SuccessResponse(baseSuccessCode.getStatus(), baseSuccessCode.getMessage(), data); } - public static SuccessResponse of(final BaseSuccessCode baseSuccessCode) { + public static SuccessResponse from(final BaseSuccessCode baseSuccessCode) { return new SuccessResponse(baseSuccessCode.getStatus(), baseSuccessCode.getMessage(), null); } } \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java b/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java index 6db39c88..af95f1ab 100644 --- a/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java @@ -18,26 +18,31 @@ public class GlobalExceptionHandler { @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(final BadRequestException e) { - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(e.getBaseErrorCode())); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e.getBaseErrorCode())); } @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleUnauthorizedException(final UnauthorizedException e) { - return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.of(e.getBaseErrorCode())); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e.getBaseErrorCode())); } @ExceptionHandler(ForbiddenException.class) public ResponseEntity handleForbiddenException(final ForbiddenException e) { - return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse.of(e.getBaseErrorCode())); + return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse.from(e.getBaseErrorCode())); } @ExceptionHandler(NotFoundException.class) protected ResponseEntity handleNotFoundException(final NotFoundException e) { - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.of(e.getBaseErrorCode())); + return ResponseEntity.status(HttpStatus.NOT_FOUND).body(ErrorResponse.from(e.getBaseErrorCode())); } @ExceptionHandler(MethodArgumentNotValidException.class) protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage())); } + + @ExceptionHandler(Exception.class) + protected ResponseEntity handleException(final Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다.")); + } } diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 29c509ce..6b37a506 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -17,4 +17,5 @@ spring: show-sql: true properties: hibernate: + dialect: org.hibernate.dialect.MySQLDialect format_sql: true \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 07dddf1a..6de7f526 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -13,8 +13,9 @@ spring: password: ${PROD_DB_PASSWORD} jpa: hibernate: - ddl-auto: validate - show-sql: false + ddl-auto: create + show-sql: true properties: hibernate: - format_sql: false \ No newline at end of file + dialect: org.hibernate.dialect.MySQLDialect + format_sql: true \ No newline at end of file From b802e5172ee230fd3ed95dafef8ec3e68b31e48e Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Fri, 12 Jul 2024 02:26:55 +0900 Subject: [PATCH 03/39] =?UTF-8?q?[feat]=20#30=20-=20SwaggerConfig=20?= =?UTF-8?q?=EB=B0=8F=20WebConfig=20=EA=B5=AC=ED=98=84=20(#31)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 --- build.gradle | 1 + .../beat/global/common/config/WebConfig.java | 18 ++++++++++++++++++ .../beat/global/swagger/SwaggerConfig.java | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 src/main/java/com/beat/global/common/config/WebConfig.java create mode 100644 src/main/java/com/beat/global/swagger/SwaggerConfig.java diff --git a/build.gradle b/build.gradle index 1b8f6063..687ff512 100644 --- a/build.gradle +++ b/build.gradle @@ -34,6 +34,7 @@ dependencies { testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' } jar { diff --git a/src/main/java/com/beat/global/common/config/WebConfig.java b/src/main/java/com/beat/global/common/config/WebConfig.java new file mode 100644 index 00000000..397df748 --- /dev/null +++ b/src/main/java/com/beat/global/common/config/WebConfig.java @@ -0,0 +1,18 @@ +package com.beat.global.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig implements WebMvcConfigurer { + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOriginPatterns("*") // 모든 도메인 허용 + .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/swagger/SwaggerConfig.java b/src/main/java/com/beat/global/swagger/SwaggerConfig.java new file mode 100644 index 00000000..587f51e0 --- /dev/null +++ b/src/main/java/com/beat/global/swagger/SwaggerConfig.java @@ -0,0 +1,19 @@ +package com.beat.global.swagger; + +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.info.Info; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SwaggerConfig { + + @Bean + public OpenAPI openAPI() { + return new OpenAPI() + .info(new Info() + .title("BEAT Project API") + .description("드랍더비트") + .version("1.0.0")); + } +} \ No newline at end of file From b50cec3352f16b271ce4e3c994919899dfd25ede Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Fri, 12 Jul 2024 02:34:49 +0900 Subject: [PATCH 04/39] =?UTF-8?q?HOTFIX(workflows):=20push=20=EC=9D=B4?= =?UTF-8?q?=EB=B2=A4=ED=8A=B8=20=EC=8B=9C=20github=20action=EC=9D=B4=20?= =?UTF-8?q?=EB=B9=8C=EB=93=9C=20=EB=90=98=EC=A7=80=20=EC=95=8A=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/dev-CI.yml | 2 -- .github/workflows/prod-CI.yml | 2 -- 2 files changed, 4 deletions(-) diff --git a/.github/workflows/dev-CI.yml b/.github/workflows/dev-CI.yml index 8343f4bd..111c2e83 100644 --- a/.github/workflows/dev-CI.yml +++ b/.github/workflows/dev-CI.yml @@ -1,8 +1,6 @@ name: dev-CI on: - push: - branches: [ "develop" ] pull_request: branches: [ "develop" ] diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index 99640230..f227a027 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -1,8 +1,6 @@ name: prod-CI on: - push: - branches: [ "main" ] pull_request: branches: [ "main" ] From 6e279c8b7e5391b862e8372da964751b7b852020 Mon Sep 17 00:00:00 2001 From: DongHoon Lee Date: Fri, 12 Jul 2024 03:37:21 +0900 Subject: [PATCH 05/39] =?UTF-8?q?HOTFIX(Jenkinsfile):=20=ED=8F=AC=ED=8A=B8?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6790a8d8..5ac08b0d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -84,7 +84,7 @@ pipeline { // 새로운 컨테이너 실행 sshCommand remote: remote, command: ( 'docker run -d --name springboot' + - ' -p 80:' + INTERNAL_PORT + + ' -p 8080:' + INTERNAL_PORT + ' -e "SPRING_PROFILES_ACTIVE=' + OPERATION_ENV + '"' + ' ' + DOCKER_IMAGE_NAME + ':latest' ) From 3c3cb9ffe5d21dae52ed791e46c6ea2b80cf33a5 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Fri, 12 Jul 2024 06:18:13 +0900 Subject: [PATCH 06/39] =?UTF-8?q?[feat]=20#32=20-=20=ED=8B=B0=EC=BC=93=20?= =?UTF-8?q?=EC=98=88=EB=A7=A4=20=EA=B0=80=EB=8A=A5=20=EC=97=AC=EB=B6=80=20?= =?UTF-8?q?GET=20API=20=EA=B5=AC=ED=98=84=20(#33)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 --- .../domain/booking/api/BookingController.java | 2 +- .../schedule/api/ScheduleController.java | 35 +++++++++++++ .../schedule/application/ScheduleService.java | 50 +++++++++++++++++++ .../dto/TicketAvailabilityRequest.java | 10 ++++ .../dto/TicketAvailabilityResponse.java | 15 ++++++ .../schedule/dao/ScheduleRepository.java | 7 +++ .../schedule/exception/ScheduleErrorCode.java | 17 +++++++ .../exception/ScheduleSuccessCode.java | 15 ++++++ .../common/exception/ConflictException.java | 7 +++ .../handler/GlobalExceptionHandler.java | 6 +++ 10 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/beat/domain/schedule/api/ScheduleController.java create mode 100644 src/main/java/com/beat/domain/schedule/application/ScheduleService.java create mode 100644 src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityRequest.java create mode 100644 src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityResponse.java create mode 100644 src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java create mode 100644 src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java create mode 100644 src/main/java/com/beat/domain/schedule/exception/ScheduleSuccessCode.java create mode 100644 src/main/java/com/beat/global/common/exception/ConflictException.java diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java index a4d73327..5d479130 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -23,7 +23,7 @@ public class BookingController { private final BookingService bookingService; @PostMapping("/guest/retrieve") - public ResponseEntity>> findGuestBookings( + public ResponseEntity>> getGuestBookings( @RequestBody BookingRetrieveRequest bookingRetrieveRequest) { List response = bookingService.findGuestBookings(bookingRetrieveRequest); return ResponseEntity.status(HttpStatus.OK) diff --git a/src/main/java/com/beat/domain/schedule/api/ScheduleController.java b/src/main/java/com/beat/domain/schedule/api/ScheduleController.java new file mode 100644 index 00000000..3e0d8982 --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/api/ScheduleController.java @@ -0,0 +1,35 @@ +package com.beat.domain.schedule.api; + +import com.beat.domain.schedule.application.ScheduleService; +import com.beat.domain.schedule.application.dto.TicketAvailabilityRequest; +import com.beat.domain.schedule.application.dto.TicketAvailabilityResponse; +import com.beat.domain.schedule.exception.ScheduleSuccessCode; +import com.beat.global.common.dto.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/schedules") +@RequiredArgsConstructor +public class ScheduleController { + + private final ScheduleService scheduleService; + + @GetMapping("/{scheduleId}/availability") + public ResponseEntity> getTicketAvailability( + @PathVariable Long scheduleId, + @RequestParam int purchaseTicketCount) { + + TicketAvailabilityRequest ticketAvailabilityRequest = TicketAvailabilityRequest.of(purchaseTicketCount); + TicketAvailabilityResponse response = scheduleService.findTicketAvailability(scheduleId, ticketAvailabilityRequest); + + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(ScheduleSuccessCode.TICKET_AVAILABILITY_RETRIEVAL_SUCCESS, response)); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java new file mode 100644 index 00000000..004b601c --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java @@ -0,0 +1,50 @@ +package com.beat.domain.schedule.application; + +import com.beat.domain.schedule.application.dto.TicketAvailabilityRequest; +import com.beat.domain.schedule.application.dto.TicketAvailabilityResponse; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.schedule.exception.ScheduleErrorCode; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.ConflictException; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ScheduleService { + + private final ScheduleRepository scheduleRepository; + + public TicketAvailabilityResponse findTicketAvailability(Long scheduleId, TicketAvailabilityRequest ticketAvailabilityRequest) { + validateRequest(scheduleId, ticketAvailabilityRequest); + + Schedule schedule = scheduleRepository.findById(scheduleId).orElseThrow( + () -> new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND) + ); + + int availableTicketCount = schedule.getTotalTicketCount() - schedule.getSoldTicketCount(); + boolean isAvailable = availableTicketCount >= ticketAvailabilityRequest.purchaseTicketCount(); + + if (!isAvailable) { + throw new ConflictException(ScheduleErrorCode.INSUFFICIENT_TICKETS); + } + + return TicketAvailabilityResponse.of( + schedule.getScheduleId(), + schedule.getScheduleNumber().getDisplayName(), + schedule.getTotalTicketCount(), + schedule.getSoldTicketCount(), + availableTicketCount, + ticketAvailabilityRequest.purchaseTicketCount(), + isAvailable + ); + } + + private void validateRequest(Long scheduleId, TicketAvailabilityRequest ticketAvailabilityRequest) { + if (ticketAvailabilityRequest.purchaseTicketCount() <= 0 || scheduleId <= 0) { + throw new BadRequestException(ScheduleErrorCode.INVALID_DATA_FORMAT); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityRequest.java b/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityRequest.java new file mode 100644 index 00000000..1341facd --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityRequest.java @@ -0,0 +1,10 @@ +package com.beat.domain.schedule.application.dto; + +public record TicketAvailabilityRequest( + Integer purchaseTicketCount + +) { + public static TicketAvailabilityRequest of(Integer purchaseTicketCount) { + return new TicketAvailabilityRequest(purchaseTicketCount); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityResponse.java b/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityResponse.java new file mode 100644 index 00000000..9b8c053d --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/application/dto/TicketAvailabilityResponse.java @@ -0,0 +1,15 @@ +package com.beat.domain.schedule.application.dto; + +public record TicketAvailabilityResponse( + Long scheduleId, + String scheduleNumber, + int totalTicketCount, + int soldTicketCount, + int availableTicketCount, + int requestedTicketCount, + boolean isAvailable +) { + public static TicketAvailabilityResponse of(Long scheduleId, String scheduleNumber, int totalTicketCount, int soldTicketCount, int availableTicketCount, int requestedTicketCount, boolean isAvailable) { + return new TicketAvailabilityResponse(scheduleId, scheduleNumber, totalTicketCount, soldTicketCount, availableTicketCount, requestedTicketCount, isAvailable); + } +} diff --git a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java new file mode 100644 index 00000000..df1a4aac --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java @@ -0,0 +1,7 @@ +package com.beat.domain.schedule.dao; + +import com.beat.domain.schedule.domain.Schedule; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface ScheduleRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java b/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java new file mode 100644 index 00000000..f35e921c --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java @@ -0,0 +1,17 @@ +package com.beat.domain.schedule.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ScheduleErrorCode implements BaseErrorCode { + INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), + NO_SCHEDULE_FOUND(404, "해당 회차를 찾을 수 없습니다."), + INSUFFICIENT_TICKETS(409, "요청한 티켓 수량이 잔여 티켓 수를 초과했습니다. 다른 수량을 선택해 주세요.") + ; + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/schedule/exception/ScheduleSuccessCode.java b/src/main/java/com/beat/domain/schedule/exception/ScheduleSuccessCode.java new file mode 100644 index 00000000..3fb245e0 --- /dev/null +++ b/src/main/java/com/beat/domain/schedule/exception/ScheduleSuccessCode.java @@ -0,0 +1,15 @@ +package com.beat.domain.schedule.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum ScheduleSuccessCode implements BaseSuccessCode { + TICKET_AVAILABILITY_RETRIEVAL_SUCCESS(200, "티켓 수량 조회가 성공적으로 완료되었습니다.") + ; + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/global/common/exception/ConflictException.java b/src/main/java/com/beat/global/common/exception/ConflictException.java new file mode 100644 index 00000000..30f09ddf --- /dev/null +++ b/src/main/java/com/beat/global/common/exception/ConflictException.java @@ -0,0 +1,7 @@ +package com.beat.global.common.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; + +public class ConflictException extends BeatException{ + public ConflictException(final BaseErrorCode baseErrorCode) {super(baseErrorCode);} +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java b/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java index af95f1ab..34c38c70 100644 --- a/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java @@ -2,6 +2,7 @@ import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.ConflictException; import com.beat.global.common.exception.ForbiddenException; import com.beat.global.common.exception.NotFoundException; import com.beat.global.common.exception.UnauthorizedException; @@ -41,6 +42,11 @@ protected ResponseEntity handleMethodArgumentNotValidException(Me return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage())); } + @ExceptionHandler(ConflictException.class) + protected ResponseEntity handleConflictException(final ConflictException e) { + return ResponseEntity.status(HttpStatus.CONFLICT).body(ErrorResponse.from(e.getBaseErrorCode())); + } + @ExceptionHandler(Exception.class) protected ResponseEntity handleException(final Exception e) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다.")); From f4d1c2060876af95130501de27164a5dca1f38e9 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Sat, 13 Jul 2024 22:39:18 +0900 Subject: [PATCH 07/39] =?UTF-8?q?[feat]=20#17=20-=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?API=20=EA=B5=AC=ED=98=84=20(#36)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> --- build.gradle | 41 +++++- src/main/java/com/beat/BeatApplication.java | 5 + .../domain/member/api/MemberController.java | 50 +++++++ .../member/application/MemberService.java | 137 ++++++++++++++++++ .../domain/member/dao/MemberRepository.java | 17 +++ .../member/dao/MemberRepositoryCustom.java | 11 ++ .../com/beat/domain/member/domain/Member.java | 49 +++++-- .../beat/domain/member/domain/SocialType.java | 12 ++ .../member/dto/AccessTokenGetSuccess.java | 11 ++ .../member/dto/LoginSuccessResponse.java | 15 ++ .../member/exception/MemberErrorCode.java | 15 ++ .../member/exception/MemberSuccessCode.java | 18 +++ .../domain/user/application/UserService.java | 4 + .../user/application/dto/UserResponse.java | 7 + .../beat/domain/user/dao/UserRepository.java | 7 + .../com/beat/domain/user/domain/Users.java | 22 +-- .../auth/client/dto/MemberInfoResponse.java | 19 +++ .../auth/client/dto/MemberLoginRequest.java | 10 ++ .../auth/client/kakao/KakaoApiClient.java | 14 ++ .../auth/client/kakao/KakaoAuthApiClient.java | 18 +++ .../response/KakaoAccessTokenResponse.java | 17 +++ .../client/kakao/response/KakaoAccount.java | 11 ++ .../kakao/response/KakaoUserProfile.java | 10 ++ .../kakao/response/KakaoUserResponse.java | 11 ++ .../client/service/KakaoSocialService.java | 84 +++++++++++ .../auth/client/service/SocialService.java | 8 + .../auth/jwt/application/TokenService.java | 40 +++++ .../global/auth/jwt/dao/TokenRepository.java | 14 ++ .../auth/jwt/exception/TokenErrorCode.java | 17 +++ .../jwt/filter/JwtAuthenticationFilter.java | 61 ++++++++ .../auth/jwt/provider/JwtTokenProvider.java | 94 ++++++++++++ .../auth/jwt/provider/JwtValidationType.java | 15 ++ .../auth/redis/LettuceLockRepository.java | 23 +++ .../com/beat/global/auth/redis/Token.java | 27 ++++ .../security/CustomAccessDeniedHandler.java | 22 +++ .../CustomJwtAuthenticationEntryPoint.java | 22 +++ .../auth/security/MemberAuthentication.java | 16 ++ .../global/common/config/RedisConfig.java | 30 ++++ .../global/common/config/SecurityConfig.java | 60 ++++++++ src/main/resources/application-dev.yml | 31 +++- src/main/resources/application-prod.yml | 32 +++- 41 files changed, 1102 insertions(+), 25 deletions(-) create mode 100644 src/main/java/com/beat/domain/member/api/MemberController.java create mode 100644 src/main/java/com/beat/domain/member/application/MemberService.java create mode 100644 src/main/java/com/beat/domain/member/dao/MemberRepository.java create mode 100644 src/main/java/com/beat/domain/member/dao/MemberRepositoryCustom.java create mode 100644 src/main/java/com/beat/domain/member/domain/SocialType.java create mode 100644 src/main/java/com/beat/domain/member/dto/AccessTokenGetSuccess.java create mode 100644 src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java create mode 100644 src/main/java/com/beat/domain/member/exception/MemberErrorCode.java create mode 100644 src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java create mode 100644 src/main/java/com/beat/domain/user/application/UserService.java create mode 100644 src/main/java/com/beat/domain/user/application/dto/UserResponse.java create mode 100644 src/main/java/com/beat/domain/user/dao/UserRepository.java create mode 100644 src/main/java/com/beat/global/auth/client/dto/MemberInfoResponse.java create mode 100644 src/main/java/com/beat/global/auth/client/dto/MemberLoginRequest.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccessTokenResponse.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccount.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserProfile.java create mode 100644 src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserResponse.java create mode 100644 src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java create mode 100644 src/main/java/com/beat/global/auth/client/service/SocialService.java create mode 100644 src/main/java/com/beat/global/auth/jwt/application/TokenService.java create mode 100644 src/main/java/com/beat/global/auth/jwt/dao/TokenRepository.java create mode 100644 src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java create mode 100644 src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java create mode 100644 src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java create mode 100644 src/main/java/com/beat/global/auth/jwt/provider/JwtValidationType.java create mode 100644 src/main/java/com/beat/global/auth/redis/LettuceLockRepository.java create mode 100644 src/main/java/com/beat/global/auth/redis/Token.java create mode 100644 src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java create mode 100644 src/main/java/com/beat/global/auth/security/MemberAuthentication.java create mode 100644 src/main/java/com/beat/global/common/config/RedisConfig.java create mode 100644 src/main/java/com/beat/global/common/config/SecurityConfig.java diff --git a/build.gradle b/build.gradle index 687ff512..eb9dbfbc 100644 --- a/build.gradle +++ b/build.gradle @@ -21,19 +21,52 @@ configurations { repositories { mavenCentral() + maven { url "https://repo.spring.io/milestone" } + maven { url "https://repo.spring.io/snapshot" } } dependencies { - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' -// implementation 'org.springframework.boot:spring-boot-starter-security' + // Spring implementation 'org.springframework.boot:spring-boot-starter-web' - compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' + + // Database runtimeOnly 'com.mysql:mysql-connector-j' + implementation 'org.springframework.boot:spring-boot-starter-data-jpa' + + // Lombok + compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // Validation + implementation 'org.springframework.boot:spring-boot-starter-validation' + + // Test testImplementation 'org.springframework.boot:spring-boot-starter-test' - testImplementation 'org.springframework.security:spring-security-test' + + // Junit testRuntimeOnly 'org.junit.platform:junit-platform-launcher' + + // External API + implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.3' + + // JWT + implementation 'io.jsonwebtoken:jjwt-api:0.11.5' + implementation 'io.jsonwebtoken:jjwt-impl:0.11.5' + implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5' + + // Redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' + implementation 'org.springframework.boot:spring-boot-starter-data-redis-reactive' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // Oauth2 + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' + + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' } diff --git a/src/main/java/com/beat/BeatApplication.java b/src/main/java/com/beat/BeatApplication.java index 66aaab6c..263f8261 100644 --- a/src/main/java/com/beat/BeatApplication.java +++ b/src/main/java/com/beat/BeatApplication.java @@ -1,9 +1,14 @@ package com.beat; import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; @SpringBootApplication +@EnableFeignClients +@ImportAutoConfiguration({FeignAutoConfiguration.class}) public class BeatApplication { public static void main(String[] args) { diff --git a/src/main/java/com/beat/domain/member/api/MemberController.java b/src/main/java/com/beat/domain/member/api/MemberController.java new file mode 100644 index 00000000..6acf52a0 --- /dev/null +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -0,0 +1,50 @@ +package com.beat.domain.member.api; + +import com.beat.domain.member.application.MemberService; +import com.beat.domain.member.dto.*; +import com.beat.domain.member.exception.MemberSuccessCode; +import com.beat.global.auth.client.dto.MemberLoginRequest; +import com.beat.global.auth.jwt.application.TokenService; +import com.beat.global.common.dto.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; + +@RestController +@RequestMapping("/api/users") +@RequiredArgsConstructor +public class MemberController { + private final MemberService memberService; + private final TokenService tokenService; + + @PostMapping("/sign-up") + public ResponseEntity> signUp( + @RequestParam final String authorizationCode, + @RequestBody final MemberLoginRequest loginRequest + ) { + LoginSuccessResponse loginSuccessResponse = memberService.create(authorizationCode, loginRequest); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, loginSuccessResponse)); + } + + @GetMapping("/refresh-token") + public ResponseEntity> refreshToken( + @RequestParam final String refreshToken + ) { + AccessTokenGetSuccess accessTokenGetSuccess = memberService.refreshToken(refreshToken); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(MemberSuccessCode.ISSUE_REFRESH_TOKEN_SUCCESS, accessTokenGetSuccess)); + } + + @PostMapping("/sign-out") + public ResponseEntity> signOut( + final Principal principal + ) { + tokenService.deleteRefreshToken(Long.valueOf(principal.getName())); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.from(MemberSuccessCode.SIGN_OUT_SUCCESS)); + } +} diff --git a/src/main/java/com/beat/domain/member/application/MemberService.java b/src/main/java/com/beat/domain/member/application/MemberService.java new file mode 100644 index 00000000..9b0bc639 --- /dev/null +++ b/src/main/java/com/beat/domain/member/application/MemberService.java @@ -0,0 +1,137 @@ +package com.beat.domain.member.application; + +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.domain.SocialType; +import com.beat.domain.member.dto.*; +import com.beat.domain.member.exception.MemberErrorCode; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.global.auth.client.dto.MemberInfoResponse; +import com.beat.global.auth.client.dto.MemberLoginRequest; +import com.beat.global.auth.client.service.KakaoSocialService; +import com.beat.global.auth.jwt.application.TokenService; +import com.beat.global.auth.jwt.exception.TokenErrorCode; +import com.beat.global.auth.jwt.provider.JwtTokenProvider; +import com.beat.global.auth.security.MemberAuthentication; +import com.beat.global.common.exception.*; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Transactional(readOnly = true) +@Service +public class MemberService { + private final UserRepository userRepository; + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; + private final KakaoSocialService kakaoSocialService; + + public LoginSuccessResponse create( + final String authorizationCode, + final MemberLoginRequest loginRequest + ) { + return getTokenDto(getUserInfoResponse(authorizationCode, loginRequest)); + } + + public MemberInfoResponse getUserInfoResponse( + final String authorizationCode, + final MemberLoginRequest loginRequest + ) { + switch (loginRequest.socialType()) { + case KAKAO: + return kakaoSocialService.login(authorizationCode, loginRequest); + default: + throw new BadRequestException(MemberErrorCode.SOCIAL_TYPE_BAD_REQUEST); + } + } + + @Transactional + public Long createUser(final MemberInfoResponse userResponse) { + // Users 엔티티를 먼저 생성 + Users users = Users.create(); + users = userRepository.save(users); + + // Users 엔티티가 생성된 후 Member 엔티티 생성 + Member member = Member.create( + userResponse.nickname(), + userResponse.email(), + users, + userResponse.socialId(), + userResponse.socialType() + ); + + memberRepository.save(member); + + return member.getId(); + } + + public Member getBySocialId( + final Long socialId, + final SocialType socialType) { + Member member = memberRepository.findBySocialTypeAndSocialId(socialId, socialType).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND) + ); + return member; + } + + public AccessTokenGetSuccess refreshToken( + final String refreshToken + ) { + Long memberId = jwtTokenProvider.getUserFromJwt(refreshToken); + if (!memberId.equals(tokenService.findIdByRefreshToken(refreshToken))) { + throw new BadRequestException(TokenErrorCode.TOKEN_INCORRECT_ERROR); + } + MemberAuthentication memberAuthentication = new MemberAuthentication(memberId, null, null); + return AccessTokenGetSuccess.of( + jwtTokenProvider.issueAccessToken(memberAuthentication) + ); + } + + public boolean isExistingMember( + final Long socialId, + final SocialType socialType + ) { + return memberRepository.findBySocialTypeAndSocialId(socialId, socialType).isPresent(); + } + + public LoginSuccessResponse getTokenByMemberId( + final Long id, + final MemberInfoResponse memberInfoResponse + ) { + MemberAuthentication memberAuthentication = new MemberAuthentication(id, null, null); + String refreshToken = jwtTokenProvider.issueRefreshToken(memberAuthentication); + tokenService.saveRefreshToken(id, refreshToken); + String nickname = memberInfoResponse.nickname(); + return LoginSuccessResponse.of( + jwtTokenProvider.issueAccessToken(memberAuthentication), + refreshToken, nickname + ); + } + + @Transactional + public void deleteUser( + final Long id + ) { + Users users = userRepository.findById(id) + .orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND) + ); + userRepository.delete(users); + } + + private LoginSuccessResponse getTokenDto( + final MemberInfoResponse userResponse + ) { + if (isExistingMember(userResponse.socialId(), userResponse.socialType())) { + return getTokenByMemberId(getBySocialId(userResponse.socialId(), userResponse.socialType()).getId(), userResponse); + } else { + Long id = createUser(userResponse); + return getTokenByMemberId(id, userResponse); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/dao/MemberRepository.java b/src/main/java/com/beat/domain/member/dao/MemberRepository.java new file mode 100644 index 00000000..efdeb795 --- /dev/null +++ b/src/main/java/com/beat/domain/member/dao/MemberRepository.java @@ -0,0 +1,17 @@ +package com.beat.domain.member.dao; + +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.domain.SocialType; +import feign.Param; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.Optional; + +public interface MemberRepository extends JpaRepository { + + @Query("SELECT u FROM Member u WHERE u.socialId = :socialId AND u.socialType = :socialType") + Optional findBySocialTypeAndSocialId(@Param("socialId") Long socialId, + @Param("socialType") SocialType socialType); + +} diff --git a/src/main/java/com/beat/domain/member/dao/MemberRepositoryCustom.java b/src/main/java/com/beat/domain/member/dao/MemberRepositoryCustom.java new file mode 100644 index 00000000..388b6402 --- /dev/null +++ b/src/main/java/com/beat/domain/member/dao/MemberRepositoryCustom.java @@ -0,0 +1,11 @@ +package com.beat.domain.member.dao; + +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.domain.SocialType; + +import java.util.Optional; + +public interface MemberRepositoryCustom { + Optional findBySocialTypeAndSocialId(final Long socialId, final SocialType socialType); + +} diff --git a/src/main/java/com/beat/domain/member/domain/Member.java b/src/main/java/com/beat/domain/member/domain/Member.java index 1c269f1a..69a17f46 100644 --- a/src/main/java/com/beat/domain/member/domain/Member.java +++ b/src/main/java/com/beat/domain/member/domain/Member.java @@ -1,12 +1,9 @@ package com.beat.domain.member.domain; import com.beat.domain.BaseTimeEntity; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; +import com.beat.domain.user.domain.Users; +import jakarta.persistence.*; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -14,12 +11,12 @@ @Entity @Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) +@NoArgsConstructor public class Member extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long memberId; + private Long id; @Column(nullable = false) private String nickname; @@ -30,6 +27,40 @@ public class Member extends BaseTimeEntity { @Column(nullable = true) private LocalDateTime deletedAt; + @ManyToOne + @JoinColumn(name = "user_id", nullable = true) + private Users user; + @Column(nullable = false) - private Long userId; + private Long socialId; // 카카오 회원번호 저장 + + @Enumerated(EnumType.STRING) + private SocialType socialType; + + + @Builder + public Member(String nickname, String email, LocalDateTime deletedAt, Users user, Long socialId, SocialType socialType) { + this.nickname = nickname; + this.email = email; + this.deletedAt = deletedAt; + this.user = user; + this.socialId = socialId; + this.socialType = socialType; + } + + public static Member create( + final String nickname, + final String email, + final Users user, + final Long socialId, + final SocialType socialType + ) { + return Member.builder() + .nickname(nickname) + .email(email) + .user(user) + .socialId(socialId) + .socialType(socialType) + .build(); + } } diff --git a/src/main/java/com/beat/domain/member/domain/SocialType.java b/src/main/java/com/beat/domain/member/domain/SocialType.java new file mode 100644 index 00000000..05042ab6 --- /dev/null +++ b/src/main/java/com/beat/domain/member/domain/SocialType.java @@ -0,0 +1,12 @@ +package com.beat.domain.member.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum SocialType { + KAKAO("KAKAO"), + ; + private final String type; +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/dto/AccessTokenGetSuccess.java b/src/main/java/com/beat/domain/member/dto/AccessTokenGetSuccess.java new file mode 100644 index 00000000..66ac3b99 --- /dev/null +++ b/src/main/java/com/beat/domain/member/dto/AccessTokenGetSuccess.java @@ -0,0 +1,11 @@ +package com.beat.domain.member.dto; + +public record AccessTokenGetSuccess( + String accessToken +) { + public static AccessTokenGetSuccess of( + final String accessToken + ) { + return new AccessTokenGetSuccess(accessToken); + } +} diff --git a/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java b/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java new file mode 100644 index 00000000..57b0576e --- /dev/null +++ b/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java @@ -0,0 +1,15 @@ +package com.beat.domain.member.dto; + +public record LoginSuccessResponse( + String accessToken, + String refreshToken, + String nickname +) { + public static LoginSuccessResponse of( + final String accessToken, + final String refreshToken, + final String nickname + ) { + return new LoginSuccessResponse(accessToken, refreshToken, nickname); + } +} diff --git a/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java b/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java new file mode 100644 index 00000000..91f2777f --- /dev/null +++ b/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java @@ -0,0 +1,15 @@ +package com.beat.domain.member.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberErrorCode implements BaseErrorCode { + MEMBER_NOT_FOUND(404, "유저가 없습니다"), + SOCIAL_TYPE_BAD_REQUEST(400, "로그인 요청이 유효하지 않습니다."); + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java b/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java new file mode 100644 index 00000000..e6337e33 --- /dev/null +++ b/src/main/java/com/beat/domain/member/exception/MemberSuccessCode.java @@ -0,0 +1,18 @@ +package com.beat.domain.member.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum MemberSuccessCode implements BaseSuccessCode { + SIGN_UP_SUCCESS(200, "로그인 성공"), + ISSUE_ACCESS_TOKEN_SUCCESS(200, "엑세스토큰 발급 성공"), + ISSUE_REFRESH_TOKEN_SUCCESS(200, "리프레쉬토큰 발급 성공"), + SIGN_OUT_SUCCESS(200, "로그아웃 성공"), + USER_DELETE_SUCCESS(200, "회원 탈퇴 성공"); + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/user/application/UserService.java b/src/main/java/com/beat/domain/user/application/UserService.java new file mode 100644 index 00000000..5c8a1ace --- /dev/null +++ b/src/main/java/com/beat/domain/user/application/UserService.java @@ -0,0 +1,4 @@ +package com.beat.domain.user.application; + +public class UserService { +} diff --git a/src/main/java/com/beat/domain/user/application/dto/UserResponse.java b/src/main/java/com/beat/domain/user/application/dto/UserResponse.java new file mode 100644 index 00000000..61df11c4 --- /dev/null +++ b/src/main/java/com/beat/domain/user/application/dto/UserResponse.java @@ -0,0 +1,7 @@ +package com.beat.domain.user.application.dto; + +public record UserResponse( + +) { + +} diff --git a/src/main/java/com/beat/domain/user/dao/UserRepository.java b/src/main/java/com/beat/domain/user/dao/UserRepository.java new file mode 100644 index 00000000..47837f89 --- /dev/null +++ b/src/main/java/com/beat/domain/user/dao/UserRepository.java @@ -0,0 +1,7 @@ +package com.beat.domain.user.dao; + +import com.beat.domain.user.domain.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} diff --git a/src/main/java/com/beat/domain/user/domain/Users.java b/src/main/java/com/beat/domain/user/domain/Users.java index 3f4371a8..7741d2bb 100644 --- a/src/main/java/com/beat/domain/user/domain/Users.java +++ b/src/main/java/com/beat/domain/user/domain/Users.java @@ -1,19 +1,23 @@ package com.beat.domain.user.domain; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import jakarta.persistence.*; +import lombok.*; @Entity @Getter -@NoArgsConstructor(access= AccessLevel.PROTECTED) +@Setter public class Users { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long userId; + private Long id; + + @Builder + public Users() { + } + + public static Users create() { + return Users.builder() + .build(); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/dto/MemberInfoResponse.java b/src/main/java/com/beat/global/auth/client/dto/MemberInfoResponse.java new file mode 100644 index 00000000..0c37eefd --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/dto/MemberInfoResponse.java @@ -0,0 +1,19 @@ +package com.beat.global.auth.client.dto; + +import com.beat.domain.member.domain.SocialType; + +public record MemberInfoResponse( + Long socialId, + String nickname, + String email, + SocialType socialType +) { + public static MemberInfoResponse of ( + final Long socialId, + final String nickname, + final String email, + final SocialType socialType + ) { + return new MemberInfoResponse(socialId, nickname, email, socialType); + } +} diff --git a/src/main/java/com/beat/global/auth/client/dto/MemberLoginRequest.java b/src/main/java/com/beat/global/auth/client/dto/MemberLoginRequest.java new file mode 100644 index 00000000..0cd91160 --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/dto/MemberLoginRequest.java @@ -0,0 +1,10 @@ +package com.beat.global.auth.client.dto; + +import com.beat.domain.member.domain.SocialType; +import jakarta.validation.constraints.NotNull; + +public record MemberLoginRequest( + @NotNull(message = "소셜 로그인 종류가 입력되지 않았습니다.") + SocialType socialType +) { +} diff --git a/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java b/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java new file mode 100644 index 00000000..d822e38b --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java @@ -0,0 +1,14 @@ +package com.beat.global.auth.client.kakao; + +import com.beat.global.auth.client.kakao.response.KakaoUserResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestHeader; + +@FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") +public interface KakaoApiClient { + + @GetMapping(value = "/v2/user/me") + KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken); +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java b/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java new file mode 100644 index 00000000..7053944f --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java @@ -0,0 +1,18 @@ +package com.beat.global.auth.client.kakao; + +import com.beat.global.auth.client.kakao.response.KakaoAccessTokenResponse; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; + +@FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") +public interface KakaoAuthApiClient { + @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + KakaoAccessTokenResponse getOAuth2AccessToken( + @RequestParam("grant_type") String grantType, + @RequestParam("client_id") String clientId, + @RequestParam("redirect_uri") String redirectUri, + @RequestParam("code") String code + ); +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccessTokenResponse.java b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccessTokenResponse.java new file mode 100644 index 00000000..73521d12 --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccessTokenResponse.java @@ -0,0 +1,17 @@ +package com.beat.global.auth.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccessTokenResponse( + String accessToken +) { + public static KakaoAccessTokenResponse of( + final String accessToken + ) { + return new KakaoAccessTokenResponse( + accessToken + ); + } +} diff --git a/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccount.java b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccount.java new file mode 100644 index 00000000..81d533b0 --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoAccount.java @@ -0,0 +1,11 @@ +package com.beat.global.auth.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoAccount( + String email, + KakaoUserProfile profile +) { +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserProfile.java b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserProfile.java new file mode 100644 index 00000000..4d113192 --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserProfile.java @@ -0,0 +1,10 @@ +package com.beat.global.auth.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserProfile( + String nickname +) { +} diff --git a/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserResponse.java b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserResponse.java new file mode 100644 index 00000000..1b859aad --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/kakao/response/KakaoUserResponse.java @@ -0,0 +1,11 @@ +package com.beat.global.auth.client.kakao.response; + +import com.fasterxml.jackson.databind.PropertyNamingStrategies; +import com.fasterxml.jackson.databind.annotation.JsonNaming; + +@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class) +public record KakaoUserResponse( + Long id, + KakaoAccount kakaoAccount +) { +} diff --git a/src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java b/src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java new file mode 100644 index 00000000..36471454 --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java @@ -0,0 +1,84 @@ +package com.beat.global.auth.client.service; + +import com.beat.domain.member.domain.SocialType; +import com.beat.global.auth.client.dto.MemberInfoResponse; +import com.beat.global.auth.client.dto.MemberLoginRequest; +import com.beat.global.auth.client.kakao.KakaoApiClient; +import com.beat.global.auth.client.kakao.KakaoAuthApiClient; +import com.beat.global.auth.client.kakao.response.KakaoAccessTokenResponse; +import com.beat.global.auth.client.kakao.response.KakaoUserResponse; +import com.beat.global.auth.jwt.exception.TokenErrorCode; +import com.beat.global.common.exception.UnauthorizedException; +import feign.FeignException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +@Service +@Slf4j +@RequiredArgsConstructor +public class KakaoSocialService implements SocialService { + + private static final String AUTH_CODE = "authorization_code"; + + @Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}") + private String REDIRECT_URI; + + @Value("${spring.security.oauth2.client.registration.kakao.client-id}") + private String clientId; + private final KakaoApiClient kakaoApiClient; + private final KakaoAuthApiClient kakaoAuthApiClient; + + + @Transactional + @Override + public MemberInfoResponse login( + final String authorizationCode, + final MemberLoginRequest loginRequest + ) { + String accessToken; + try { + // 인가 코드로 Access Token + Refresh Token 받아오기 + accessToken = getOAuth2Authentication(authorizationCode); + } catch (FeignException e) { + throw new UnauthorizedException(TokenErrorCode.AUTHENTICATION_CODE_EXPIRED); + } + // Access Token으로 유저 정보 불러오기 + return getLoginDto(loginRequest.socialType(), getUserInfo(accessToken)); + } + + private String getOAuth2Authentication( + final String authorizationCode + ) { + KakaoAccessTokenResponse response = kakaoAuthApiClient.getOAuth2AccessToken( + AUTH_CODE, + clientId, + REDIRECT_URI, + authorizationCode + ); + log.info("Received OAuth2 authentication response: {}", response); + return response.accessToken(); + } + + private KakaoUserResponse getUserInfo( + final String accessToken + ) { + KakaoUserResponse kakaoUserResponse = kakaoApiClient.getUserInformation("Bearer " + accessToken); + return kakaoUserResponse; + } + + private MemberInfoResponse getLoginDto( + final SocialType socialType, + final KakaoUserResponse kakaoUserResponse + ) { + return MemberInfoResponse.of( + kakaoUserResponse.id(), + kakaoUserResponse.kakaoAccount().profile().nickname(), + kakaoUserResponse.kakaoAccount().email(), + socialType + ); + } + +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/service/SocialService.java b/src/main/java/com/beat/global/auth/client/service/SocialService.java new file mode 100644 index 00000000..3934d2ef --- /dev/null +++ b/src/main/java/com/beat/global/auth/client/service/SocialService.java @@ -0,0 +1,8 @@ +package com.beat.global.auth.client.service; + +import com.beat.global.auth.client.dto.MemberInfoResponse; +import com.beat.global.auth.client.dto.MemberLoginRequest; + +public interface SocialService { + MemberInfoResponse login(final String authorizationToken, final MemberLoginRequest loginRequest); +} diff --git a/src/main/java/com/beat/global/auth/jwt/application/TokenService.java b/src/main/java/com/beat/global/auth/jwt/application/TokenService.java new file mode 100644 index 00000000..6097bfeb --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/application/TokenService.java @@ -0,0 +1,40 @@ +package com.beat.global.auth.jwt.application; + +import com.beat.global.auth.jwt.dao.TokenRepository; +import com.beat.global.auth.jwt.exception.TokenErrorCode; +import com.beat.global.auth.redis.Token; +import com.beat.global.common.exception.NotFoundException; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@RequiredArgsConstructor +@Service +public class TokenService { + + private final TokenRepository tokenRepository; + + @Transactional + public void saveRefreshToken(final Long memberId, final String refreshToken) { + tokenRepository.save( + Token.of(memberId, refreshToken) + ); + } + + public Long findIdByRefreshToken(final String refreshToken) { + Token token = tokenRepository.findByRefreshToken(refreshToken) + .orElseThrow( + () -> new NotFoundException(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND) + ); + return token.getId(); + } + + @Transactional + public void deleteRefreshToken(final Long memberId) { + Token token = tokenRepository.findById(memberId) + .orElseThrow( + () -> new NotFoundException(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND) + ); + tokenRepository.delete(token); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/jwt/dao/TokenRepository.java b/src/main/java/com/beat/global/auth/jwt/dao/TokenRepository.java new file mode 100644 index 00000000..6a798272 --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/dao/TokenRepository.java @@ -0,0 +1,14 @@ +package com.beat.global.auth.jwt.dao; + + +import java.util.Optional; + +import com.beat.global.auth.redis.Token; +import org.springframework.data.repository.CrudRepository; + +public interface TokenRepository extends CrudRepository { + + Optional findByRefreshToken(final String refreshToken); + + Optional findById(final Long id); +} diff --git a/src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java b/src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java new file mode 100644 index 00000000..c8ab889a --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java @@ -0,0 +1,17 @@ +package com.beat.global.auth.jwt.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TokenErrorCode implements BaseErrorCode { + + AUTHENTICATION_CODE_EXPIRED(403, "토큰이 만료되었습니다"), + REFRESH_TOKEN_NOT_FOUND(404, "리프레쉬토큰이 없습니다"), + TOKEN_INCORRECT_ERROR(400, "잘못된 토큰입니다"); + + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java new file mode 100644 index 00000000..cb5a6e24 --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java @@ -0,0 +1,61 @@ +package com.beat.global.auth.jwt.filter; + + +import com.beat.global.auth.jwt.provider.JwtTokenProvider; +import com.beat.global.auth.security.MemberAuthentication; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.NonNull; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +import static com.beat.global.auth.jwt.provider.JwtValidationType.VALID_JWT; + +@Slf4j +@Component +@RequiredArgsConstructor +public class JwtAuthenticationFilter extends OncePerRequestFilter { + + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + try { + final String token = getJwtFromRequest(request); + if (jwtTokenProvider.validateToken(token) == VALID_JWT) { + Long memberId = jwtTokenProvider.getUserFromJwt(token); + // authentication 객체 생성 -> principal에 유저정보를 담는다. + MemberAuthentication authentication = new MemberAuthentication(memberId.toString(), null, null); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } catch (Exception exception) { + try { + + throw new Exception(); + } catch (Exception e) { + log.info(e.getMessage()); + } + } + // 다음 필터로 요청 전달 + filterChain.doFilter(request, response); + } + + private String getJwtFromRequest(HttpServletRequest request) { + String bearerToken = request.getHeader("Authorization"); + if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { + return bearerToken.substring("Bearer ".length()); + } + return null; + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java new file mode 100644 index 00000000..f1c0908d --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -0,0 +1,94 @@ +package com.beat.global.auth.jwt.provider; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.ExpiredJwtException; +import io.jsonwebtoken.Header; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.security.Keys; +import jakarta.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; +import java.util.Base64; +import java.util.Date; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +@Service +public class JwtTokenProvider { + + @Value("${jwt.secret}") + private String JWT_SECRET; + @Value("${jwt.access-token-expire-time}") + private long ACCESS_TOKEN_EXPIRE_TIME; + @Value("${jwt.refresh-token-expire-time}") + private long REFRESH_TOKEN_EXPIRE_TIME; + + private static final String MEMBER_ID = "memberId"; + + @PostConstruct + protected void init() { + JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + } + + public String issueAccessToken(final Authentication authentication) { + return issueToken(authentication, ACCESS_TOKEN_EXPIRE_TIME); + } + + public String issueRefreshToken(final Authentication authentication) { + return issueToken(authentication, REFRESH_TOKEN_EXPIRE_TIME); + } + + private String issueToken( + final Authentication authentication, + final Long expiredTime + ) { + final Date now = new Date(); + + final Claims claims = Jwts.claims() + .setIssuedAt(now) + .setExpiration(new Date(now.getTime() + expiredTime)); + + claims.put(MEMBER_ID, authentication.getPrincipal()); + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateToken(String token) { + try { + final Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + return JwtValidationType.EMPTY_JWT; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder() + .setSigningKey(getSigningKey()) + .build() + .parseClaimsJws(token) + .getBody(); + } + + public Long getUserFromJwt(String token) { + Claims claims = getBody(token); + return Long.valueOf(claims.get(MEMBER_ID).toString()); + } +} diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtValidationType.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtValidationType.java new file mode 100644 index 00000000..fa6fafcd --- /dev/null +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtValidationType.java @@ -0,0 +1,15 @@ +package com.beat.global.auth.jwt.provider; + +import lombok.AllArgsConstructor; + +@AllArgsConstructor +public enum JwtValidationType { + VALID_JWT("VALID_JWT"), // 유효한 JWT + INVALID_JWT_SIGNATURE("INVALID_JWT_SIGNATURE"), // 유효하지 않은 서명 + INVALID_JWT_TOKEN("INVALID_JWT_TOKEN"), // 유효하지 않은 토큰 + EXPIRED_JWT_TOKEN("EXPIRED_JWT_TOKEN"), // 만료된 토큰 + UNSUPPORTED_JWT_TOKEN("UNSUPPORTED_JWT_TOKEN"), // 지원하지 않는 형식의 토큰 + EMPTY_JWT("EMPTY_JWT"); // 빈 JWT + + private String validationType; +} diff --git a/src/main/java/com/beat/global/auth/redis/LettuceLockRepository.java b/src/main/java/com/beat/global/auth/redis/LettuceLockRepository.java new file mode 100644 index 00000000..7a4b37ec --- /dev/null +++ b/src/main/java/com/beat/global/auth/redis/LettuceLockRepository.java @@ -0,0 +1,23 @@ +package com.beat.global.auth.redis; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Repository; + +import java.time.Duration; + +@RequiredArgsConstructor +@Repository +public class LettuceLockRepository { + private final RedisTemplate redisTemplate; + + public Boolean lock(String token, String lockType) { + return redisTemplate + .opsForValue() + .setIfAbsent(token, lockType, Duration.ofSeconds(3L)); + } + + public void unlock(String token) { + redisTemplate.delete(token); + } +} diff --git a/src/main/java/com/beat/global/auth/redis/Token.java b/src/main/java/com/beat/global/auth/redis/Token.java new file mode 100644 index 00000000..6eab8fce --- /dev/null +++ b/src/main/java/com/beat/global/auth/redis/Token.java @@ -0,0 +1,27 @@ +package com.beat.global.auth.redis; + +import jakarta.persistence.Id; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import org.springframework.data.redis.core.RedisHash; +import org.springframework.data.redis.core.index.Indexed; + +@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 1000L * 14) +@Getter +@Builder +public class Token { + + @Id + private Long id; + + @Indexed + private String refreshToken; + + public static Token of(final Long id, final String refreshToken) { + return Token.builder() + .id(id) + .refreshToken(refreshToken) + .build(); + } +} diff --git a/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java b/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java new file mode 100644 index 00000000..65b67724 --- /dev/null +++ b/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java @@ -0,0 +1,22 @@ +package com.beat.global.auth.security; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +@Component +public class CustomAccessDeniedHandler implements AccessDeniedHandler { + @Override + public void handle(HttpServletRequest request, HttpServletResponse response, + AccessDeniedException accessDeniedException) throws IOException, ServletException { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_FORBIDDEN); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java b/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java new file mode 100644 index 00000000..55f6ef33 --- /dev/null +++ b/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java @@ -0,0 +1,22 @@ +package com.beat.global.auth.security; + +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; + +@Component +public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, + AuthenticationException authException) { + setResponse(response); + } + + private void setResponse(HttpServletResponse response) { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + } + +} diff --git a/src/main/java/com/beat/global/auth/security/MemberAuthentication.java b/src/main/java/com/beat/global/auth/security/MemberAuthentication.java new file mode 100644 index 00000000..9a63514f --- /dev/null +++ b/src/main/java/com/beat/global/auth/security/MemberAuthentication.java @@ -0,0 +1,16 @@ +package com.beat.global.auth.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class MemberAuthentication extends UsernamePasswordAuthenticationToken { + + // 사용자 인증 객체 생성 + public MemberAuthentication(Object principal, Object credentials, + Collection authorities) { + super(principal, credentials, authorities); + } +} + diff --git a/src/main/java/com/beat/global/common/config/RedisConfig.java b/src/main/java/com/beat/global/common/config/RedisConfig.java new file mode 100644 index 00000000..a48ef4ae --- /dev/null +++ b/src/main/java/com/beat/global/common/config/RedisConfig.java @@ -0,0 +1,30 @@ +package com.beat.global.common.config; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.core.RedisTemplate; + +@Configuration +public class RedisConfig { + + @Value("${spring.data.redis.host}") + private String host; + + @Value("${spring.data.redis.port}") + private int port; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + return new LettuceConnectionFactory(host, port); + } + + @Bean + public RedisTemplate redisTemplate() { + RedisTemplate redisTemplate = new RedisTemplate<>(); + redisTemplate.setConnectionFactory(redisConnectionFactory()); + return redisTemplate; + } +} diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java new file mode 100644 index 00000000..58aa1cba --- /dev/null +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -0,0 +1,60 @@ +package com.beat.global.common.config; + +import com.beat.global.auth.jwt.filter.JwtAuthenticationFilter; +import com.beat.global.auth.security.CustomAccessDeniedHandler; +import com.beat.global.auth.security.CustomJwtAuthenticationEntryPoint; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + +@EnableWebSecurity +@RequiredArgsConstructor +@Configuration +public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; + private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; + private final CustomAccessDeniedHandler customAccessDeniedHandler; + + private static final String[] AUTH_WHITELIST = { + "/api/users/sign-up", + "/api/users/refresh-token", + "/api/users/sign-out", + "/api/v1/actuator/health", + "/api/v1/v3/api-docs/**", + "/api/v1/swagger-ui/**", + "/api/v1/swagger-resources/**", +// "/login/oauth2/code/kakao", +// "/kakao/callback" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http.csrf(AbstractHttpConfigurer::disable) + .formLogin(AbstractHttpConfigurer::disable) + .httpBasic(AbstractHttpConfigurer::disable) + .sessionManagement(session -> { + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); + }) + .exceptionHandling(exception -> + { + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); + exception.accessDeniedHandler(customAccessDeniedHandler); + }); + + http.authorizeHttpRequests(auth -> { + auth.requestMatchers(AUTH_WHITELIST).permitAll(); + auth.anyRequest().authenticated(); + }) + .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); + + return http.build(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 6b37a506..5bd60c54 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -11,6 +11,7 @@ spring: url: ${DEV_DB_URL} username: ${DEV_DB_USERNAME} password: ${DEV_DB_PASSWORD} + jpa: hibernate: ddl-auto: update @@ -18,4 +19,32 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect - format_sql: true \ No newline at end of file + format_sql: true + + data: + redis: + host: localhost + port: 6379 + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + +jwt: + secret: ${JWT_SECRET} + access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 + refresh-token-expire-time: 1209600000 # 14일 밀리초 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 6de7f526..e3d993a1 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -14,8 +14,36 @@ spring: jpa: hibernate: ddl-auto: create - show-sql: true + show-sql: false properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect - format_sql: true \ No newline at end of file + format_sql: true + + data: + redis: + host: localhost + port: 6379 + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + +jwt: + secret: ${JWT_SECRET} + access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 + refresh-token-expire-time: 1209600000 # 14일 밀리초 From 467dab0a51f38f905917da00af59736286ce5db3 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Sun, 14 Jul 2024 13:25:53 +0900 Subject: [PATCH 08/39] =?UTF-8?q?[feat]=20#34=20-=20=EB=B9=84=ED=9A=8C?= =?UTF-8?q?=EC=9B=90=20=EC=98=88=EB=A7=A4=20POST=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#35)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 --- .../domain/booking/api/BookingController.java | 18 +- ...rvice.java => BookingRetrieveService.java} | 8 +- .../application/GuestBookingService.java | 86 ++++++++ .../application/dto/GuestBookingRequest.java | 17 ++ .../application/dto/GuestBookingResponse.java | 46 ++++ .../domain/booking/dao/BookingRepository.java | 7 + .../beat/domain/booking/domain/Booking.java | 2 +- .../booking/exception/BookingSuccessCode.java | 1 + .../com/beat/domain/cast/domain/Cast.java | 2 +- .../dao/PerformanceRepository.java | 7 + .../performance/domain/Performance.java | 4 +- .../schedule/application/ScheduleService.java | 2 +- .../schedule/dao/ScheduleRepository.java | 10 + .../beat/domain/schedule/domain/Schedule.java | 4 +- .../com/beat/domain/staff/domain/Staff.java | 2 +- .../com/beat/domain/user/domain/Users.java | 9 +- .../user/repository/UserRepository.java | 7 + src/main/resources/application-local.yml | 5 +- .../GuestBookingServiceConcurrencyTest.java | 206 ++++++++++++++++++ 19 files changed, 425 insertions(+), 18 deletions(-) rename src/main/java/com/beat/domain/booking/application/{BookingService.java => BookingRetrieveService.java} (95%) create mode 100644 src/main/java/com/beat/domain/booking/application/GuestBookingService.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java create mode 100644 src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java create mode 100644 src/main/java/com/beat/domain/user/repository/UserRepository.java create mode 100644 src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java index 5d479130..6b3573e6 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -1,8 +1,11 @@ package com.beat.domain.booking.api; -import com.beat.domain.booking.application.BookingService; +import com.beat.domain.booking.application.BookingRetrieveService; +import com.beat.domain.booking.application.GuestBookingService; import com.beat.domain.booking.application.dto.BookingRetrieveRequest; import com.beat.domain.booking.application.dto.BookingRetrieveResponse; +import com.beat.domain.booking.application.dto.GuestBookingRequest; +import com.beat.domain.booking.application.dto.GuestBookingResponse; import com.beat.domain.booking.exception.BookingSuccessCode; import com.beat.global.common.dto.SuccessResponse; import lombok.RequiredArgsConstructor; @@ -20,12 +23,21 @@ @RequiredArgsConstructor public class BookingController { - private final BookingService bookingService; + private final BookingRetrieveService bookingRetrieveService; + private final GuestBookingService guestBookingService; + + @PostMapping("/guest") + public ResponseEntity> createGuestBookings( + @RequestBody GuestBookingRequest guestBookingRequest) { + GuestBookingResponse response = guestBookingService.createGuestBooking(guestBookingRequest); + return ResponseEntity.status(HttpStatus.CREATED) + .body(SuccessResponse.of(BookingSuccessCode.BOOKING_SUCCESS, response)); + } @PostMapping("/guest/retrieve") public ResponseEntity>> getGuestBookings( @RequestBody BookingRetrieveRequest bookingRetrieveRequest) { - List response = bookingService.findGuestBookings(bookingRetrieveRequest); + List response = bookingRetrieveService.findGuestBookings(bookingRetrieveRequest); return ResponseEntity.status(HttpStatus.OK) .body(SuccessResponse.of(BookingSuccessCode.BOOKING_RETRIEVE_SUCCESS, response)); } diff --git a/src/main/java/com/beat/domain/booking/application/BookingService.java b/src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java similarity index 95% rename from src/main/java/com/beat/domain/booking/application/BookingService.java rename to src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java index 9780dd65..2a9f401d 100644 --- a/src/main/java/com/beat/domain/booking/application/BookingService.java +++ b/src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java @@ -17,7 +17,7 @@ @Service @RequiredArgsConstructor -public class BookingService { +public class BookingRetrieveService { private final BookingRepository bookingRepository; @@ -65,8 +65,8 @@ private BookingRetrieveResponse toBookingResponse(Booking booking) { Performance performance = schedule.getPerformance(); return BookingRetrieveResponse.of( - booking.getBookingId(), - schedule.getScheduleId(), + booking.getId(), + schedule.getId(), performance.getPerformanceTitle(), schedule.getPerformanceDate(), performance.getPerformanceVenue(), @@ -76,7 +76,7 @@ private BookingRetrieveResponse toBookingResponse(Booking booking) { booking.getBookerPhoneNumber(), performance.getBankName().name(), performance.getAccountNumber(), - schedule.getScheduleId().intValue(), + schedule.getId().intValue(), booking.isPaymentCompleted(), booking.getCreatedAt() ); diff --git a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java new file mode 100644 index 00000000..faa14461 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java @@ -0,0 +1,86 @@ +package com.beat.domain.booking.application; + +import com.beat.domain.booking.application.dto.GuestBookingRequest; +import com.beat.domain.booking.application.dto.GuestBookingResponse; +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.schedule.exception.ScheduleErrorCode; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.repository.UserRepository; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class GuestBookingService { + + private static final Logger logger = LoggerFactory.getLogger(GuestBookingService.class); + + private final ScheduleRepository scheduleRepository; + private final BookingRepository bookingRepository; + private final UserRepository userRepository; + + @Transactional + public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingRequest) { + Schedule schedule = scheduleRepository.lockById(guestBookingRequest.scheduleId()) + .orElseThrow(() -> new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND)); + + int availableTicketCount = schedule.getTotalTicketCount() - schedule.getSoldTicketCount(); + if (availableTicketCount < guestBookingRequest.purchaseTicketCount()) { + throw new BadRequestException(ScheduleErrorCode.INSUFFICIENT_TICKETS); + } + + schedule.setSoldTicketCount(schedule.getSoldTicketCount() + guestBookingRequest.purchaseTicketCount()); + + Users users = bookingRepository.findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPassword( + guestBookingRequest.bookerName(), + guestBookingRequest.bookerPhoneNumber(), + guestBookingRequest.birthDate(), + guestBookingRequest.password() + ).map(Booking::getUsers).orElseGet(() -> { + Users newUser = Users.create(); + userRepository.save(newUser); + return newUser; + }); + + int ticketPrice = schedule.getPerformance().getTicketPrice(); + int totalPaymentAmount = ticketPrice * guestBookingRequest.purchaseTicketCount(); + scheduleRepository.save(schedule); + + Booking booking = Booking.create( + guestBookingRequest.purchaseTicketCount(), + guestBookingRequest.bookerName(), + guestBookingRequest.bookerPhoneNumber(), + guestBookingRequest.isPaymentCompleted(), + guestBookingRequest.birthDate(), + guestBookingRequest.password(), + schedule, + users + ); + bookingRepository.save(booking); + + logger.info("Booking created: {}", booking); + + return GuestBookingResponse.of( + booking.getId(), + booking.getSchedule().getId(), + booking.getUsers().getId(), + booking.getPurchaseTicketCount(), + booking.getSchedule().getScheduleNumber().getDisplayName(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + booking.isPaymentCompleted(), + booking.getSchedule().getPerformance().getBankName().getDisplayName(), + booking.getSchedule().getPerformance().getAccountNumber(), + totalPaymentAmount, + booking.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java new file mode 100644 index 00000000..b9cde400 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java @@ -0,0 +1,17 @@ +package com.beat.domain.booking.application.dto; + +public record GuestBookingRequest( + Long scheduleId, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + String birthDate, + String password, + int totalPaymentAmount, + boolean isPaymentCompleted +) { + public static GuestBookingRequest of(Long scheduleId, int purchaseTicketCount, String scheduleNumber, String bookerName, String bookerPhoneNumber, String birthDate, String password, int totalPaymentAmount, boolean isPaymentCompleted) { + return new GuestBookingRequest(scheduleId, purchaseTicketCount, scheduleNumber, bookerName, bookerPhoneNumber, birthDate, password, totalPaymentAmount, isPaymentCompleted); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java new file mode 100644 index 00000000..b457f1c5 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java @@ -0,0 +1,46 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record GuestBookingResponse( + Long bookingId, + Long scheduleId, + Long userId, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + String bankName, + String accountNumber, + int totalPaymentAmount, + LocalDateTime createdAt +) { + public static GuestBookingResponse of( + Long bookingId, + Long scheduleId, + Long userId, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + String bankName, + String accountNumber, + int totalPaymentAmount, + LocalDateTime createdAt) { + return new GuestBookingResponse( + bookingId, + scheduleId, + userId, + purchaseTicketCount, + scheduleNumber, + bookerName, + bookerPhoneNumber, + isPaymentCompleted, + bankName, + accountNumber, + totalPaymentAmount, + createdAt); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java index f5e98889..89bdc5da 100644 --- a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java +++ b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java @@ -13,4 +13,11 @@ Optional> findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthD String password, String birthDate ); + + Optional findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPassword( + String bookerName, + String bookerPhoneNumber, + String birthDate, + String password + ); } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/domain/Booking.java b/src/main/java/com/beat/domain/booking/domain/Booking.java index ef1d2ce5..16487a74 100644 --- a/src/main/java/com/beat/domain/booking/domain/Booking.java +++ b/src/main/java/com/beat/domain/booking/domain/Booking.java @@ -26,7 +26,7 @@ public class Booking { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long bookingId; + private Long id; @Column(nullable = false) private int purchaseTicketCount; diff --git a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java index 074c47c5..4ed71621 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -7,6 +7,7 @@ @Getter @RequiredArgsConstructor public enum BookingSuccessCode implements BaseSuccessCode { + BOOKING_SUCCESS(200, "비회원 예매가 성공적으로 완료되었습니다"), BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") ; diff --git a/src/main/java/com/beat/domain/cast/domain/Cast.java b/src/main/java/com/beat/domain/cast/domain/Cast.java index fdba6e43..6ad8dc2d 100644 --- a/src/main/java/com/beat/domain/cast/domain/Cast.java +++ b/src/main/java/com/beat/domain/cast/domain/Cast.java @@ -16,7 +16,7 @@ public class Cast{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long castId; + private Long id; @Column(nullable = false) private String castName; diff --git a/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java new file mode 100644 index 00000000..acb20231 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java @@ -0,0 +1,7 @@ +package com.beat.domain.performance.dao; + +import com.beat.domain.performance.domain.Performance; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PerformanceRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/domain/Performance.java b/src/main/java/com/beat/domain/performance/domain/Performance.java index b24554a3..1226b990 100644 --- a/src/main/java/com/beat/domain/performance/domain/Performance.java +++ b/src/main/java/com/beat/domain/performance/domain/Performance.java @@ -17,7 +17,7 @@ public class Performance extends BaseTimeEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long performanceId; + private Long id; @Column(nullable = false) private String performanceTitle; @@ -64,7 +64,7 @@ public class Performance extends BaseTimeEntity { private int totalScheduleCount; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) + @JoinColumn(name = "user_id", nullable = true) // 테스트를 위한 false @OnDelete(action = OnDeleteAction.CASCADE) private Users users; diff --git a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java index 004b601c..18f18824 100644 --- a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java +++ b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java @@ -32,7 +32,7 @@ public TicketAvailabilityResponse findTicketAvailability(Long scheduleId, Ticket } return TicketAvailabilityResponse.of( - schedule.getScheduleId(), + schedule.getId(), schedule.getScheduleNumber().getDisplayName(), schedule.getTotalTicketCount(), schedule.getSoldTicketCount(), diff --git a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java index df1a4aac..a6c87afa 100644 --- a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java +++ b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java @@ -1,7 +1,17 @@ package com.beat.domain.schedule.dao; import com.beat.domain.schedule.domain.Schedule; +import jakarta.persistence.LockModeType; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.Optional; public interface ScheduleRepository extends JpaRepository { + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @Query("SELECT s FROM Schedule s WHERE s.id = :id") + Optional lockById(@Param("id") Long id); } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/domain/Schedule.java b/src/main/java/com/beat/domain/schedule/domain/Schedule.java index 2f26eca1..d78de579 100644 --- a/src/main/java/com/beat/domain/schedule/domain/Schedule.java +++ b/src/main/java/com/beat/domain/schedule/domain/Schedule.java @@ -15,17 +15,19 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.time.LocalDateTime; @Entity @Getter +@Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Schedule { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long scheduleId; + private Long id; @Column(nullable = false) private LocalDateTime performanceDate; diff --git a/src/main/java/com/beat/domain/staff/domain/Staff.java b/src/main/java/com/beat/domain/staff/domain/Staff.java index edd6e9d6..4bc0101e 100644 --- a/src/main/java/com/beat/domain/staff/domain/Staff.java +++ b/src/main/java/com/beat/domain/staff/domain/Staff.java @@ -16,7 +16,7 @@ public class Staff { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long staffId; + private Long id; @Column(nullable = false) private String staffName; diff --git a/src/main/java/com/beat/domain/user/domain/Users.java b/src/main/java/com/beat/domain/user/domain/Users.java index 7741d2bb..b2fc9059 100644 --- a/src/main/java/com/beat/domain/user/domain/Users.java +++ b/src/main/java/com/beat/domain/user/domain/Users.java @@ -1,7 +1,12 @@ package com.beat.domain.user.domain; -import jakarta.persistence.*; -import lombok.*; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; @Entity @Getter diff --git a/src/main/java/com/beat/domain/user/repository/UserRepository.java b/src/main/java/com/beat/domain/user/repository/UserRepository.java new file mode 100644 index 00000000..6a7c7f99 --- /dev/null +++ b/src/main/java/com/beat/domain/user/repository/UserRepository.java @@ -0,0 +1,7 @@ +package com.beat.domain.user.repository; + +import com.beat.domain.user.domain.Users; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface UserRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index abfdeb03..c923e123 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -10,11 +10,12 @@ spring: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/beat username: root - password: 192401 + password: jpa: hibernate: - ddl-auto: update + ddl-auto: create show-sql: true properties: hibernate: + dialect: org.hibernate.dialect.MySQLDialect format_sql: true \ No newline at end of file diff --git a/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java b/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java new file mode 100644 index 00000000..d0e42892 --- /dev/null +++ b/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java @@ -0,0 +1,206 @@ +package com.beat.domain.booking; + +import com.beat.domain.booking.application.GuestBookingService; +import com.beat.domain.booking.application.dto.GuestBookingRequest; +import com.beat.domain.booking.application.dto.GuestBookingResponse; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.BankName; +import com.beat.domain.performance.domain.Genre; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.schedule.domain.ScheduleNumber; +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.user.repository.UserRepository; +import com.beat.domain.user.domain.Users; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.ThreadLocalRandom; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.fail; + +@SpringBootTest +public class GuestBookingServiceConcurrencyTest { + + private static final Logger logger = LoggerFactory.getLogger(GuestBookingServiceConcurrencyTest.class); + + @Autowired + private GuestBookingService guestBookingService; + + @Autowired + private ScheduleRepository scheduleRepository; + + @Autowired + private PerformanceRepository performanceRepository; + + @Autowired + private BookingRepository bookingRepository; + + @Autowired + private UserRepository userRepository; + + private Schedule schedule1; + private Schedule schedule2; + + @BeforeEach + @Transactional + public void setup() { + logger.info("Setting up initial data..."); + + Users initialUser = Users.create(); + userRepository.save(initialUser); + + logger.info("Setting up userId = 1 메이커"); + + + Performance performance = Performance.create( + "Performance Title", + Genre.BAND, + 120, + "Performance Description", + "Performance Attention Note", + BankName.BUSAN, + "2342-234234-2344", + "poster.jpg", + "Performance Team", + "Performance Venue", + "010-1234-5678", + "2024-01-01 to 2024-12-31", + 35000, + 1, + initialUser + ); + performanceRepository.save(performance); + + LocalDateTime performanceDate = LocalDateTime.now().plusDays(1); + schedule1 = Schedule.create( + performanceDate, + 10, // 남은 티켓 10매 + 0, + true, + ScheduleNumber.FIRST, + performance + ); + scheduleRepository.save(schedule1); + + schedule2 = Schedule.create( + performanceDate, + 1, // 남은 티켓 1매 + 0, + true, + ScheduleNumber.SECOND, + performance + ); + scheduleRepository.save(schedule2); + + logger.info("Setup completed."); + } + + @Test + public void testConcurrentGuestBooking() { + int threadCount1 = 100; // 회차 1번에 대해 100명 요청 + int threadCount2 = 150; // 회차 2번에 대해 150명 요청 + + ExecutorService executorService1 = Executors.newFixedThreadPool(threadCount1); + ExecutorService executorService2 = Executors.newFixedThreadPool(threadCount2); + + for (int i = 0; i < threadCount1; i++) { + executorService1.submit(() -> { + try { + GuestBookingRequest request = GuestBookingRequest.of( + schedule1.getId(), // 회차 1번 스케줄 ID를 사용 + 2, // purchaseTicketCount + "FIRST", + "서지우", + "010-2222-7196", + "1990-01-01", + generateRandomPassword(), + 35000, + false + ); + GuestBookingResponse response = guestBookingService.createGuestBooking(request); + assertNotNull(response); + } catch (Exception e) { + logger.error("Exception occurred during booking for schedule 1: ", e); + fail("Exception occurred: " + e.getMessage()); + } + }); + } + + for (int i = 0; i < threadCount2; i++) { + executorService2.submit(() -> { + try { + GuestBookingRequest request = GuestBookingRequest.of( + schedule2.getId(), // 회차 2번 스케줄 ID를 사용 + 1, // purchaseTicketCount + "SECOND", + "서지우", + "010-2222-7196", + "1990-01-01", + generateRandomPassword(), + 35000, + false + ); + GuestBookingResponse response = guestBookingService.createGuestBooking(request); + assertNotNull(response); + } catch (Exception e) { + logger.error("Exception occurred during booking for schedule 2: ", e); + fail("Exception occurred: " + e.getMessage()); + } + }); + } + + // 모든 태스크가 완료될 때까지 대기 + executorService1.shutdown(); + executorService2.shutdown(); + try { + if (!executorService1.awaitTermination(60, TimeUnit.SECONDS)) { + executorService1.shutdownNow(); + } + if (!executorService2.awaitTermination(60, TimeUnit.SECONDS)) { + executorService2.shutdownNow(); + } + } catch (InterruptedException e) { + executorService1.shutdownNow(); + executorService2.shutdownNow(); + } + + // 로그로 예매된 티켓 수 확인 + Schedule finalSchedule1 = scheduleRepository.findById(schedule1.getId()).orElse(null); + Schedule finalSchedule2 = scheduleRepository.findById(schedule2.getId()).orElse(null); + + if (finalSchedule1 != null) { + logger.info("Total tickets sold for schedule 1: {}", finalSchedule1.getSoldTicketCount()); + } + if (finalSchedule2 != null) { + logger.info("Total tickets sold for schedule 2: {}", finalSchedule2.getSoldTicketCount()); + } + + // 예매된 유저와 예약 정보 로그 출력 + bookingRepository.findAll().forEach(booking -> { + logger.info("Booking ID: {}, User ID: {}, Schedule ID: {}, Tickets: {}", + booking.getId(), booking.getUsers().getId(), + booking.getSchedule().getId(), booking.getPurchaseTicketCount()); + }); + userRepository.findAll().forEach(user -> { + logger.info("User ID: {}", user.getId()); + }); + } + + private String generateRandomPassword() { + int randomNum = ThreadLocalRandom.current().nextInt(1000, 10000); + return String.format("%04d", randomNum); + } +} \ No newline at end of file From f55e0232d4b82805539b0c8800fb57b43917d069 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Sun, 14 Jul 2024 18:13:15 +0900 Subject: [PATCH 09/39] =?UTF-8?q?[Refactor]=20#37=20-=20=EC=B9=B4=EC=B9=B4?= =?UTF-8?q?=EC=98=A4=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20response=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#39)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 --- .../application/GuestBookingService.java | 2 +- .../application/dto/TicketRetrieveResponse.java | 2 ++ .../domain/booking/dao/TicketRepository.java | 2 ++ .../domain/member/api/MemberController.java | 17 +++++++++++++++-- .../member/application/MemberService.java | 2 ++ ...UserRepository.java => UsersRepository.java} | 2 +- .../GuestBookingServiceConcurrencyTest.java | 2 +- 7 files changed, 24 insertions(+), 5 deletions(-) create mode 100644 src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java create mode 100644 src/main/java/com/beat/domain/booking/dao/TicketRepository.java rename src/main/java/com/beat/domain/user/repository/{UserRepository.java => UsersRepository.java} (67%) diff --git a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java index faa14461..4fe9a377 100644 --- a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java @@ -7,8 +7,8 @@ import com.beat.domain.schedule.dao.ScheduleRepository; import com.beat.domain.schedule.domain.Schedule; import com.beat.domain.schedule.exception.ScheduleErrorCode; +import com.beat.domain.user.dao.UserRepository; import com.beat.domain.user.domain.Users; -import com.beat.domain.user.repository.UserRepository; import com.beat.global.common.exception.BadRequestException; import com.beat.global.common.exception.NotFoundException; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java new file mode 100644 index 00000000..db46a030 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java @@ -0,0 +1,2 @@ +package com.beat.domain.booking.application.dto;public record TicketRetrieveResponse() { +} diff --git a/src/main/java/com/beat/domain/booking/dao/TicketRepository.java b/src/main/java/com/beat/domain/booking/dao/TicketRepository.java new file mode 100644 index 00000000..2c134c0c --- /dev/null +++ b/src/main/java/com/beat/domain/booking/dao/TicketRepository.java @@ -0,0 +1,2 @@ +package com.beat.domain.booking.dao;public interface TicketRepository { +} diff --git a/src/main/java/com/beat/domain/member/api/MemberController.java b/src/main/java/com/beat/domain/member/api/MemberController.java index 6acf52a0..f28f6be9 100644 --- a/src/main/java/com/beat/domain/member/api/MemberController.java +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -6,8 +6,10 @@ import com.beat.global.auth.client.dto.MemberLoginRequest; import com.beat.global.auth.jwt.application.TokenService; import com.beat.global.common.dto.SuccessResponse; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseCookie; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -19,15 +21,26 @@ public class MemberController { private final MemberService memberService; private final TokenService tokenService; + private final static int COOKIE_MAX_AGE = 7 * 24 * 60 * 60; + private final static String REFRESH_TOKEN = "refreshToken"; @PostMapping("/sign-up") public ResponseEntity> signUp( @RequestParam final String authorizationCode, - @RequestBody final MemberLoginRequest loginRequest + @RequestBody final MemberLoginRequest loginRequest, + HttpServletResponse response ) { LoginSuccessResponse loginSuccessResponse = memberService.create(authorizationCode, loginRequest); + ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN, loginSuccessResponse.refreshToken()) + .maxAge(COOKIE_MAX_AGE) + .path("/") + .secure(true) + .sameSite("None") + .httpOnly(true) + .build(); + response.setHeader("Set-Cookie", cookie.toString()); return ResponseEntity.status(HttpStatus.OK) - .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, loginSuccessResponse)); + .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, LoginSuccessResponse.of(loginSuccessResponse.accessToken(), null, loginSuccessResponse.nickname()))); } @GetMapping("/refresh-token") diff --git a/src/main/java/com/beat/domain/member/application/MemberService.java b/src/main/java/com/beat/domain/member/application/MemberService.java index 9b0bc639..c4535cd5 100644 --- a/src/main/java/com/beat/domain/member/application/MemberService.java +++ b/src/main/java/com/beat/domain/member/application/MemberService.java @@ -31,6 +31,7 @@ public class MemberService { private final TokenService tokenService; private final KakaoSocialService kakaoSocialService; + @Transactional public LoginSuccessResponse create( final String authorizationCode, final MemberLoginRequest loginRequest @@ -79,6 +80,7 @@ public Member getBySocialId( return member; } + @Transactional public AccessTokenGetSuccess refreshToken( final String refreshToken ) { diff --git a/src/main/java/com/beat/domain/user/repository/UserRepository.java b/src/main/java/com/beat/domain/user/repository/UsersRepository.java similarity index 67% rename from src/main/java/com/beat/domain/user/repository/UserRepository.java rename to src/main/java/com/beat/domain/user/repository/UsersRepository.java index 6a7c7f99..cf274de7 100644 --- a/src/main/java/com/beat/domain/user/repository/UserRepository.java +++ b/src/main/java/com/beat/domain/user/repository/UsersRepository.java @@ -3,5 +3,5 @@ import com.beat.domain.user.domain.Users; import org.springframework.data.jpa.repository.JpaRepository; -public interface UserRepository extends JpaRepository { +public interface UsersRepository extends JpaRepository { } \ No newline at end of file diff --git a/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java b/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java index d0e42892..bc851dac 100644 --- a/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java +++ b/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java @@ -11,7 +11,7 @@ import com.beat.domain.schedule.domain.Schedule; import com.beat.domain.schedule.domain.ScheduleNumber; import com.beat.domain.booking.dao.BookingRepository; -import com.beat.domain.user.repository.UserRepository; +import com.beat.domain.user.dao.UserRepository; import com.beat.domain.user.domain.Users; import org.junit.jupiter.api.BeforeEach; From 550a64d82a4acd671fb8a23b9fc0df8be0446be8 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Sun, 14 Jul 2024 22:51:51 +0900 Subject: [PATCH 10/39] =?UTF-8?q?[feat]=20#40=20-=20=EC=86=8C=EA=B0=9C=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B3=B5=EC=97=B0=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?=EC=A1=B0=ED=9A=8C=20GET=20API=20=EA=B5=AC=ED=98=84=20(#41)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename --- .../beat/domain/cast/dao/CastRepository.java | 10 +++ .../api/PerformanceController.java | 27 +++++++ .../application/PerformanceService.java | 76 +++++++++++++++++++ .../dto/PerformanceDetailCast.java | 12 +++ .../dto/PerformanceDetailResponse.java | 43 +++++++++++ .../dto/PerformanceDetailSchedule.java | 13 ++++ .../dto/PerformanceDetailStaff.java | 12 +++ .../exception/PerformanceErrorCode.java | 14 ++++ .../exception/PerformanceSuccessCode.java | 14 ++++ .../schedule/dao/ScheduleRepository.java | 3 + .../domain/staff/dao/StaffRepository.java | 10 +++ .../global/common/config/SecurityConfig.java | 2 + 12 files changed, 236 insertions(+) create mode 100644 src/main/java/com/beat/domain/cast/dao/CastRepository.java create mode 100644 src/main/java/com/beat/domain/performance/api/PerformanceController.java create mode 100644 src/main/java/com/beat/domain/performance/application/PerformanceService.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailResponse.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailSchedule.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailStaff.java create mode 100644 src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java create mode 100644 src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java create mode 100644 src/main/java/com/beat/domain/staff/dao/StaffRepository.java diff --git a/src/main/java/com/beat/domain/cast/dao/CastRepository.java b/src/main/java/com/beat/domain/cast/dao/CastRepository.java new file mode 100644 index 00000000..2fe757ff --- /dev/null +++ b/src/main/java/com/beat/domain/cast/dao/CastRepository.java @@ -0,0 +1,10 @@ +package com.beat.domain.cast.dao; + +import com.beat.domain.cast.domain.Cast; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface CastRepository extends JpaRepository { + List findByPerformanceId(Long performanceId); +} diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceController.java b/src/main/java/com/beat/domain/performance/api/PerformanceController.java new file mode 100644 index 00000000..4d865bc7 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -0,0 +1,27 @@ +package com.beat.domain.performance.api; + +import com.beat.domain.performance.application.dto.PerformanceDetailResponse; +import com.beat.domain.performance.exception.PerformanceSuccessCode; +import com.beat.domain.performance.application.PerformanceService; +import com.beat.global.common.dto.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/performances") +@RequiredArgsConstructor +public class PerformanceController { + + private final PerformanceService performanceService; + + @GetMapping("/detail/{performanceId}") + public ResponseEntity> getPerformanceDetail( + @PathVariable Long performanceId) { + PerformanceDetailResponse performanceDetail = performanceService.getPerformanceDetail(performanceId); + return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_RETRIEVE_SUCCESS, performanceDetail)); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceService.java b/src/main/java/com/beat/domain/performance/application/PerformanceService.java new file mode 100644 index 00000000..1d797e97 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -0,0 +1,76 @@ +package com.beat.domain.performance.application; + +import com.beat.domain.performance.application.dto.PerformanceDetailCast; +import com.beat.domain.performance.application.dto.PerformanceDetailResponse; +import com.beat.domain.performance.application.dto.PerformanceDetailSchedule; +import com.beat.domain.performance.application.dto.PerformanceDetailStaff; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.exception.PerformanceErrorCode; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.cast.dao.CastRepository; +import com.beat.domain.staff.dao.StaffRepository; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PerformanceService { + + private final PerformanceRepository performanceRepository; + private final ScheduleRepository scheduleRepository; + private final CastRepository castRepository; + private final StaffRepository staffRepository; + + @Transactional(readOnly = true) + public PerformanceDetailResponse getPerformanceDetail(Long performanceId) { + Performance performance = performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + + List scheduleList = scheduleRepository.findByPerformanceId(performanceId).stream() + .map(schedule -> PerformanceDetailSchedule.of( + schedule.getId(), + schedule.getPerformanceDate(), + schedule.getScheduleNumber().name() + )).collect(Collectors.toList()); + + List castList = castRepository.findByPerformanceId(performanceId).stream() + .map(cast -> PerformanceDetailCast.of( + cast.getId(), + cast.getCastName(), + cast.getCastRole(), + cast.getCastPhoto() + )).collect(Collectors.toList()); + + List staffList = staffRepository.findByPerformanceId(performanceId).stream() + .map(staff -> PerformanceDetailStaff.of( + staff.getId(), + staff.getStaffName(), + staff.getStaffRole(), + staff.getStaffPhoto() + )).collect(Collectors.toList()); + + return PerformanceDetailResponse.of( + performance.getId(), + performance.getPerformanceTitle(), + performance.getPerformancePeriod(), + scheduleList, + performance.getTicketPrice(), + performance.getGenre().name(), + performance.getPosterImage(), + performance.getRunningTime(), + performance.getPerformanceVenue(), + performance.getPerformanceDescription(), + performance.getPerformanceAttentionNote(), + performance.getPerformanceContact(), + performance.getPerformanceTeamName(), + castList, + staffList + ); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java new file mode 100644 index 00000000..2ce813eb --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto; + +public record PerformanceDetailCast( + Long castId, + String castName, + String castRole, + String castPhoto +) { + public static PerformanceDetailCast of(Long castId, String castName, String castRole, String castPhoto) { + return new PerformanceDetailCast(castId, castName, castRole, castPhoto); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailResponse.java b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailResponse.java new file mode 100644 index 00000000..a8f48a30 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailResponse.java @@ -0,0 +1,43 @@ +package com.beat.domain.performance.application.dto; + +import java.util.List; + +public record PerformanceDetailResponse( + Long performanceId, + String performanceTitle, + String performancePeriod, + List scheduleList, + int ticketPrice, + String genre, + String posterImage, + int runningTime, + String performanceVenue, + String performanceDescription, + String performanceAttentionNote, + String performanceContact, + String performanceTeamName, + List castList, + List staffList +) { + public static PerformanceDetailResponse of( + Long performanceId, + String performanceTitle, + String performancePeriod, + List scheduleList, + int ticketPrice, + String genre, + String posterImage, + int runningTime, + String performanceVenue, + String performanceDescription, + String performanceAttentionNote, + String performanceContact, + String performanceTeamName, + List castList, + List staffList + ) { + return new PerformanceDetailResponse(performanceId, performanceTitle, performancePeriod, scheduleList, ticketPrice, genre, posterImage, runningTime, performanceVenue, performanceDescription, performanceAttentionNote, performanceContact, performanceTeamName, castList, staffList); + } + + +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailSchedule.java b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailSchedule.java new file mode 100644 index 00000000..dcfe54c7 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailSchedule.java @@ -0,0 +1,13 @@ +package com.beat.domain.performance.application.dto; + +import java.time.LocalDateTime; + +public record PerformanceDetailSchedule( + Long scheduleId, + LocalDateTime performanceDate, + String scheduleNumber +) { + public static PerformanceDetailSchedule of(Long scheduleId, LocalDateTime performanceDate, String scheduleNumber) { + return new PerformanceDetailSchedule(scheduleId, performanceDate, scheduleNumber); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailStaff.java b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailStaff.java new file mode 100644 index 00000000..cf088eaf --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailStaff.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto; + +public record PerformanceDetailStaff( + Long staffId, + String staffName, + String staffRole, + String staffPhoto +) { + public static PerformanceDetailStaff of(Long staffId, String staffName, String staffRole, String staffPhoto) { + return new PerformanceDetailStaff(staffId, staffName, staffRole, staffPhoto); + } +} diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java new file mode 100644 index 00000000..fed3a9dc --- /dev/null +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java @@ -0,0 +1,14 @@ +package com.beat.domain.performance.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PerformanceErrorCode implements BaseErrorCode { + PERFORMANCE_NOT_FOUND(404, "해당 공연 정보를 찾을 수 없습니다."); + + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java new file mode 100644 index 00000000..40cb1f85 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java @@ -0,0 +1,14 @@ +package com.beat.domain.performance.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PerformanceSuccessCode implements BaseSuccessCode { + PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."); + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java index a6c87afa..c3748263 100644 --- a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java +++ b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java @@ -7,6 +7,7 @@ import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface ScheduleRepository extends JpaRepository { @@ -14,4 +15,6 @@ public interface ScheduleRepository extends JpaRepository { @Lock(LockModeType.PESSIMISTIC_WRITE) @Query("SELECT s FROM Schedule s WHERE s.id = :id") Optional lockById(@Param("id") Long id); + + List findByPerformanceId(Long performanceId); } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/staff/dao/StaffRepository.java b/src/main/java/com/beat/domain/staff/dao/StaffRepository.java new file mode 100644 index 00000000..c1acd2eb --- /dev/null +++ b/src/main/java/com/beat/domain/staff/dao/StaffRepository.java @@ -0,0 +1,10 @@ +package com.beat.domain.staff.dao; + +import com.beat.domain.staff.domain.Staff; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface StaffRepository extends JpaRepository { + List findByPerformanceId(Long performanceId); +} diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 58aa1cba..6ab98d2d 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -6,6 +6,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; @@ -31,6 +32,7 @@ public class SecurityConfig { "/api/v1/v3/api-docs/**", "/api/v1/swagger-ui/**", "/api/v1/swagger-resources/**", + "/api/performances/detail/**" // "/login/oauth2/code/kakao", // "/kakao/callback" }; From 295172ccf87e22414e2c993d42ab0e02a37adc48 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Mon, 15 Jul 2024 01:42:10 +0900 Subject: [PATCH 11/39] =?UTF-8?q?[feat]=20#42=20-=20=EC=98=88=EB=A7=A4=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EA=B3=B5=EC=97=B0=20=EC=A0=95=EB=B3=B4=20?= =?UTF-8?q?GET=20API=20=EA=B5=AC=ED=98=84=20(#43)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 --- .../api/PerformanceController.java | 8 ++++ .../application/PerformanceService.java | 37 +++++++++++++++++-- .../dto/BookingPerformanceDetailResponse.java | 30 +++++++++++++++ .../dto/BookingPerformanceDetailSchedule.java | 16 ++++++++ .../exception/PerformanceSuccessCode.java | 3 +- .../schedule/application/ScheduleService.java | 23 +++++++++++- .../global/common/config/SecurityConfig.java | 3 +- 7 files changed, 113 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailSchedule.java diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceController.java b/src/main/java/com/beat/domain/performance/api/PerformanceController.java index 4d865bc7..72c14749 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceController.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -1,5 +1,6 @@ package com.beat.domain.performance.api; +import com.beat.domain.performance.application.dto.BookingPerformanceDetailResponse; import com.beat.domain.performance.application.dto.PerformanceDetailResponse; import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.domain.performance.application.PerformanceService; @@ -24,4 +25,11 @@ public ResponseEntity> getPerformance PerformanceDetailResponse performanceDetail = performanceService.getPerformanceDetail(performanceId); return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_RETRIEVE_SUCCESS, performanceDetail)); } + + @GetMapping("/booking/{performanceId}") + public ResponseEntity> getBookingPerformanceDetail( + @PathVariable Long performanceId) { + BookingPerformanceDetailResponse bookingPerformanceDetail = performanceService.getBookingPerformanceDetail(performanceId); + return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.BOOKING_PERFORMANCE_RETRIEVE_SUCCESS, bookingPerformanceDetail)); + } } diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceService.java b/src/main/java/com/beat/domain/performance/application/PerformanceService.java index 1d797e97..2ac48266 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -1,12 +1,10 @@ package com.beat.domain.performance.application; -import com.beat.domain.performance.application.dto.PerformanceDetailCast; -import com.beat.domain.performance.application.dto.PerformanceDetailResponse; -import com.beat.domain.performance.application.dto.PerformanceDetailSchedule; -import com.beat.domain.performance.application.dto.PerformanceDetailStaff; +import com.beat.domain.performance.application.dto.*; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; import com.beat.domain.performance.exception.PerformanceErrorCode; +import com.beat.domain.schedule.application.ScheduleService; import com.beat.domain.schedule.dao.ScheduleRepository; import com.beat.domain.cast.dao.CastRepository; import com.beat.domain.staff.dao.StaffRepository; @@ -26,6 +24,7 @@ public class PerformanceService { private final ScheduleRepository scheduleRepository; private final CastRepository castRepository; private final StaffRepository staffRepository; + private final ScheduleService scheduleService; @Transactional(readOnly = true) public PerformanceDetailResponse getPerformanceDetail(Long performanceId) { @@ -73,4 +72,34 @@ public PerformanceDetailResponse getPerformanceDetail(Long performanceId) { staffList ); } + + @Transactional + public BookingPerformanceDetailResponse getBookingPerformanceDetail(Long performanceId) { + Performance performance = performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + + List scheduleList = scheduleRepository.findByPerformanceId(performanceId).stream() + .map(schedule -> { + scheduleService.updateBookingStatus(schedule); + return BookingPerformanceDetailSchedule.of( + schedule.getId(), + schedule.getPerformanceDate(), + schedule.getScheduleNumber().name(), + scheduleService.getAvailableTicketCount(schedule), + schedule.isBooking() + ); + }).collect(Collectors.toList()); + + return BookingPerformanceDetailResponse.of( + performance.getId(), + performance.getPerformanceTitle(), + performance.getPerformancePeriod(), + scheduleList, + performance.getTicketPrice(), + performance.getGenre().name(), + performance.getPosterImage(), + performance.getPerformanceVenue(), + performance.getPerformanceTeamName() + ); + } } diff --git a/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java b/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java new file mode 100644 index 00000000..7582c1a4 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java @@ -0,0 +1,30 @@ +package com.beat.domain.performance.application.dto; + +import java.util.List; + +public record BookingPerformanceDetailResponse( + Long performanceId, + String performanceTitle, + String performancePeriod, + List scheduleList, + int ticketPrice, + String genre, + String posterImage, + String performanceVenue, + String performanceTeamName +) { + public static BookingPerformanceDetailResponse of( + Long performanceId, + String performanceTitle, + String performancePeriod, + List scheduleList, + int ticketPrice, + String genre, + String posterImage, + String performanceVenue, + String performanceTeamName + ) { + return new BookingPerformanceDetailResponse(performanceId, performanceTitle, performancePeriod, scheduleList, ticketPrice, genre, posterImage, performanceVenue, performanceTeamName); + } + +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailSchedule.java b/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailSchedule.java new file mode 100644 index 00000000..0cbe3616 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailSchedule.java @@ -0,0 +1,16 @@ +package com.beat.domain.performance.application.dto; + +import java.time.LocalDateTime; + +public record BookingPerformanceDetailSchedule( + Long scheduleId, + LocalDateTime performanceDate, + String scheduleNumber, + int availableTicketCount, + boolean isBooking +) { + public static BookingPerformanceDetailSchedule of(Long scheduleId, LocalDateTime performanceDate, String scheduleNumber, int availableTicketCount, boolean isBooking) { + return new BookingPerformanceDetailSchedule(scheduleId, performanceDate, scheduleNumber, availableTicketCount, isBooking); + } + +} diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java index 40cb1f85..723a20a6 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java @@ -7,7 +7,8 @@ @Getter @RequiredArgsConstructor public enum PerformanceSuccessCode implements BaseSuccessCode { - PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."); + PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."), + BOOKING_PERFORMANCE_RETRIEVE_SUCCESS(200, "예매 관련 공연 정보 조회가 성공적으로 완료되었습니다."); private final int status; private final String message; diff --git a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java index 18f18824..dddbc949 100644 --- a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java +++ b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java @@ -10,6 +10,9 @@ import com.beat.global.common.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; @Service @RequiredArgsConstructor @@ -24,7 +27,7 @@ public TicketAvailabilityResponse findTicketAvailability(Long scheduleId, Ticket () -> new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND) ); - int availableTicketCount = schedule.getTotalTicketCount() - schedule.getSoldTicketCount(); + int availableTicketCount = getAvailableTicketCount(schedule); boolean isAvailable = availableTicketCount >= ticketAvailabilityRequest.purchaseTicketCount(); if (!isAvailable) { @@ -47,4 +50,22 @@ private void validateRequest(Long scheduleId, TicketAvailabilityRequest ticketAv throw new BadRequestException(ScheduleErrorCode.INVALID_DATA_FORMAT); } } + + public int getAvailableTicketCount(Schedule schedule) { + return schedule.getTotalTicketCount() - schedule.getSoldTicketCount(); + } + + public boolean isBookingAvailable(Schedule schedule) { + int availableTicketCount = getAvailableTicketCount(schedule); + return schedule.isBooking() && availableTicketCount > 0 && schedule.getPerformanceDate().isAfter(LocalDateTime.now()); + } + + @Transactional + public void updateBookingStatus(Schedule schedule) { + boolean isBookingAvailable = isBookingAvailable(schedule); + if (schedule.isBooking() != isBookingAvailable) { + schedule.setBooking(isBookingAvailable); + scheduleRepository.save(schedule); + } + } } \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 6ab98d2d..0443cb7c 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -32,7 +32,8 @@ public class SecurityConfig { "/api/v1/v3/api-docs/**", "/api/v1/swagger-ui/**", "/api/v1/swagger-resources/**", - "/api/performances/detail/**" + "/api/performances/detail/**", + "/api/performances/booking/**", // "/login/oauth2/code/kakao", // "/kakao/callback" }; From 86e4f6d35d9a529f10cd18ac9d6c6f93607125ad Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Mon, 15 Jul 2024 04:44:11 +0900 Subject: [PATCH 12/39] =?UTF-8?q?[feat]=20#38=20-=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=98=88=EB=A7=A4=20POST=20API=20=EA=B5=AC=ED=98=84=20(#45)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 --- .../domain/booking/api/BookingController.java | 16 +++- .../application/GuestBookingService.java | 8 +- .../application/MemberBookingService.java | 77 +++++++++++++++++++ .../application/dto/MemberBookingRequest.java | 31 ++++++++ .../dto/MemberBookingResponse.java | 48 ++++++++++++ .../booking/exception/BookingSuccessCode.java | 3 +- .../com/beat/domain/cast/domain/Cast.java | 29 ++++++- .../domain/member/api/MemberController.java | 2 +- .../member/exception/MemberErrorCode.java | 2 +- .../domain/user/exception/UserErrorCode.java | 15 ++++ .../global/auth/annotation/CurrentMember.java | 13 ++++ .../resolver/CurrentUserArgumentResolver.java | 31 ++++++++ .../beat/global/common/config/WebConfig.java | 13 ++++ src/main/resources/application-dev.yml | 36 ++++----- 14 files changed, 295 insertions(+), 29 deletions(-) create mode 100644 src/main/java/com/beat/domain/booking/application/MemberBookingService.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java create mode 100644 src/main/java/com/beat/domain/user/exception/UserErrorCode.java create mode 100644 src/main/java/com/beat/global/auth/annotation/CurrentMember.java create mode 100644 src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java index 6b3573e6..76d6ba7e 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -2,11 +2,15 @@ import com.beat.domain.booking.application.BookingRetrieveService; import com.beat.domain.booking.application.GuestBookingService; +import com.beat.domain.booking.application.MemberBookingService; import com.beat.domain.booking.application.dto.BookingRetrieveRequest; import com.beat.domain.booking.application.dto.BookingRetrieveResponse; import com.beat.domain.booking.application.dto.GuestBookingRequest; import com.beat.domain.booking.application.dto.GuestBookingResponse; +import com.beat.domain.booking.application.dto.MemberBookingRequest; +import com.beat.domain.booking.application.dto.MemberBookingResponse; import com.beat.domain.booking.exception.BookingSuccessCode; +import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.SuccessResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -25,13 +29,23 @@ public class BookingController { private final BookingRetrieveService bookingRetrieveService; private final GuestBookingService guestBookingService; + private final MemberBookingService memberBookingService; + + @PostMapping("/member") + public ResponseEntity> createMemberBooking( + @CurrentMember Long memberId, + @RequestBody MemberBookingRequest memberBookingRequest) { + MemberBookingResponse response = memberBookingService.createMemberBooking(memberId, memberBookingRequest); + return ResponseEntity.status(HttpStatus.CREATED) + .body(SuccessResponse.of(BookingSuccessCode.MEMBER_BOOKING_SUCCESS, response)); + } @PostMapping("/guest") public ResponseEntity> createGuestBookings( @RequestBody GuestBookingRequest guestBookingRequest) { GuestBookingResponse response = guestBookingService.createGuestBooking(guestBookingRequest); return ResponseEntity.status(HttpStatus.CREATED) - .body(SuccessResponse.of(BookingSuccessCode.BOOKING_SUCCESS, response)); + .body(SuccessResponse.of(BookingSuccessCode.GUEST_BOOKING_SUCCESS, response)); } @PostMapping("/guest/retrieve") diff --git a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java index 4fe9a377..93502077 100644 --- a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java @@ -70,15 +70,15 @@ public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingR return GuestBookingResponse.of( booking.getId(), - booking.getSchedule().getId(), + schedule.getId(), booking.getUsers().getId(), booking.getPurchaseTicketCount(), - booking.getSchedule().getScheduleNumber().getDisplayName(), + schedule.getScheduleNumber().getDisplayName(), booking.getBookerName(), booking.getBookerPhoneNumber(), booking.isPaymentCompleted(), - booking.getSchedule().getPerformance().getBankName().getDisplayName(), - booking.getSchedule().getPerformance().getAccountNumber(), + schedule.getPerformance().getBankName().getDisplayName(), + schedule.getPerformance().getAccountNumber(), totalPaymentAmount, booking.getCreatedAt() ); diff --git a/src/main/java/com/beat/domain/booking/application/MemberBookingService.java b/src/main/java/com/beat/domain/booking/application/MemberBookingService.java new file mode 100644 index 00000000..27e8999d --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/MemberBookingService.java @@ -0,0 +1,77 @@ +package com.beat.domain.booking.application; + +import com.beat.domain.booking.application.dto.MemberBookingRequest; +import com.beat.domain.booking.application.dto.MemberBookingResponse; +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.exception.MemberErrorCode; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.schedule.exception.ScheduleErrorCode; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class MemberBookingService { + + private final ScheduleRepository scheduleRepository; + private final BookingRepository bookingRepository; + private final MemberRepository memberRepository; + private final UserRepository userRepository; + + @Transactional(timeout = 200) + public MemberBookingResponse createMemberBooking(Long memberId, MemberBookingRequest memberBookingRequest) { + Schedule schedule = scheduleRepository.lockById(memberBookingRequest.scheduleId()) + .orElseThrow(() -> new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND)); + + int availableTicketCount = schedule.getTotalTicketCount() - schedule.getSoldTicketCount(); + if (availableTicketCount < memberBookingRequest.purchaseTicketCount()) { + throw new BadRequestException(ScheduleErrorCode.INSUFFICIENT_TICKETS); + } + + schedule.setSoldTicketCount(schedule.getSoldTicketCount() + memberBookingRequest.purchaseTicketCount()); + + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + Booking booking = Booking.create( + memberBookingRequest.purchaseTicketCount(), + memberBookingRequest.bookerName(), + memberBookingRequest.bookerPhoneNumber(), + memberBookingRequest.isPaymentCompleted(), + null, + null, + schedule, + user + ); + bookingRepository.save(booking); + scheduleRepository.save(schedule); + + return MemberBookingResponse.of( + booking.getId(), + schedule.getId(), + member.getId(), + booking.getPurchaseTicketCount(), + schedule.getScheduleNumber().getDisplayName(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + booking.isPaymentCompleted(), + schedule.getPerformance().getBankName().name(), + schedule.getPerformance().getAccountNumber(), + memberBookingRequest.totalPaymentAmount(), + booking.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java new file mode 100644 index 00000000..30c3ea79 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java @@ -0,0 +1,31 @@ +package com.beat.domain.booking.application.dto; + +public record MemberBookingRequest( + Long scheduleId, + String scheduleNumber, + int purchaseTicketCount, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + int totalPaymentAmount +) { + public static MemberBookingRequest of( + Long scheduleId, + String scheduleNumber, + int purchaseTicketCount, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + int totalPaymentAmount + ) { + return new MemberBookingRequest( + scheduleId, + scheduleNumber, + purchaseTicketCount, + bookerName, + bookerPhoneNumber, + isPaymentCompleted, + totalPaymentAmount + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java new file mode 100644 index 00000000..3aba69b7 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java @@ -0,0 +1,48 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record MemberBookingResponse( + Long bookingId, + Long scheduleId, + Long userId, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + String bankName, + String accountNumber, + int totalPaymentAmount, + LocalDateTime createdAt +) { + public static MemberBookingResponse of( + Long bookingId, + Long scheduleId, + Long userId, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + boolean isPaymentCompleted, + String bankName, + String accountNumber, + int totalPaymentAmount, + LocalDateTime createdAt + ) { + return new MemberBookingResponse( + bookingId, + scheduleId, + userId, + purchaseTicketCount, + scheduleNumber, + bookerName, + bookerPhoneNumber, + isPaymentCompleted, + bankName, + accountNumber, + totalPaymentAmount, + createdAt + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java index 4ed71621..9fc1a7f8 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -7,7 +7,8 @@ @Getter @RequiredArgsConstructor public enum BookingSuccessCode implements BaseSuccessCode { - BOOKING_SUCCESS(200, "비회원 예매가 성공적으로 완료되었습니다"), + MEMBER_BOOKING_SUCCESS(201, "회원 예매가 성공적으로 완료되었습니다"), + GUEST_BOOKING_SUCCESS(201, "비회원 예매가 성공적으로 완료되었습니다"), BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") ; diff --git a/src/main/java/com/beat/domain/cast/domain/Cast.java b/src/main/java/com/beat/domain/cast/domain/Cast.java index 6ad8dc2d..6577f5b4 100644 --- a/src/main/java/com/beat/domain/cast/domain/Cast.java +++ b/src/main/java/com/beat/domain/cast/domain/Cast.java @@ -1,11 +1,16 @@ package com.beat.domain.cast.domain; +import com.beat.domain.performance.domain.Performance; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -27,6 +32,24 @@ public class Cast{ @Column(nullable = false) private String castPhoto; - @Column(nullable = false) - private Long performanceId; -} + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = false) + private Performance performance; + + @Builder + public Cast(String castName, String castRole, String castPhoto, Performance performance) { + this.castName = castName; + this.castRole = castRole; + this.castPhoto = castPhoto; + this.performance = performance; + } + + public static Cast create(String castName, String castRole, String castPhoto, Performance performance) { + return Cast.builder() + .castName(castName) + .castRole(castRole) + .castPhoto(castPhoto) + .performance(performance) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/api/MemberController.java b/src/main/java/com/beat/domain/member/api/MemberController.java index f28f6be9..6ec5c406 100644 --- a/src/main/java/com/beat/domain/member/api/MemberController.java +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -60,4 +60,4 @@ public ResponseEntity> signOut( return ResponseEntity.status(HttpStatus.OK) .body(SuccessResponse.from(MemberSuccessCode.SIGN_OUT_SUCCESS)); } -} +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java b/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java index 91f2777f..c9533da5 100644 --- a/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java +++ b/src/main/java/com/beat/domain/member/exception/MemberErrorCode.java @@ -7,7 +7,7 @@ @Getter @RequiredArgsConstructor public enum MemberErrorCode implements BaseErrorCode { - MEMBER_NOT_FOUND(404, "유저가 없습니다"), + MEMBER_NOT_FOUND(404, "회원이 없습니다"), SOCIAL_TYPE_BAD_REQUEST(400, "로그인 요청이 유효하지 않습니다."); private final int status; diff --git a/src/main/java/com/beat/domain/user/exception/UserErrorCode.java b/src/main/java/com/beat/domain/user/exception/UserErrorCode.java new file mode 100644 index 00000000..4c7ebc40 --- /dev/null +++ b/src/main/java/com/beat/domain/user/exception/UserErrorCode.java @@ -0,0 +1,15 @@ +package com.beat.domain.user.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum UserErrorCode implements BaseErrorCode { + USER_NOT_FOUND(404, "유저가 없습니다") + ; + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/global/auth/annotation/CurrentMember.java b/src/main/java/com/beat/global/auth/annotation/CurrentMember.java new file mode 100644 index 00000000..8651ab1c --- /dev/null +++ b/src/main/java/com/beat/global/auth/annotation/CurrentMember.java @@ -0,0 +1,13 @@ +package com.beat.global.auth.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.PARAMETER) +@Retention(RetentionPolicy.RUNTIME) +@Documented +public @interface CurrentMember { +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java b/src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java new file mode 100644 index 00000000..e14e4f37 --- /dev/null +++ b/src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java @@ -0,0 +1,31 @@ +package com.beat.global.auth.resolver; + +import com.beat.global.auth.annotation.CurrentMember; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.web.bind.support.WebDataBinderFactory; +import org.springframework.web.context.request.NativeWebRequest; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; +import org.springframework.web.method.support.ModelAndViewContainer; + +@Component +@RequiredArgsConstructor +public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.getParameterAnnotation(CurrentMember.class) != null && parameter.getParameterType().equals(Long.class); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + if (authentication == null || !authentication.isAuthenticated()) { + return null; + } + return Long.valueOf(authentication.getPrincipal().toString()); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/config/WebConfig.java b/src/main/java/com/beat/global/common/config/WebConfig.java index 397df748..ce53c953 100644 --- a/src/main/java/com/beat/global/common/config/WebConfig.java +++ b/src/main/java/com/beat/global/common/config/WebConfig.java @@ -1,12 +1,25 @@ package com.beat.global.common.config; +import com.beat.global.auth.resolver.CurrentUserArgumentResolver; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + @Configuration +@RequiredArgsConstructor public class WebConfig implements WebMvcConfigurer { + private final CurrentUserArgumentResolver currentUserArgumentResolver; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(currentUserArgumentResolver); + } + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 5bd60c54..b1daf617 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -25,24 +25,24 @@ spring: redis: host: localhost port: 6379 - security: - oauth2: - client: - registration: - kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - redirect-uri: ${KAKAO_REDIRECT_URI} - client-authentication-method: POST - authorization-grant-type: authorization_code - scope: profile_nickname, account_email - client-name: Kakao - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + redirect-uri: ${KAKAO_REDIRECT_URI} + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id jwt: secret: ${JWT_SECRET} From 911c7519e4183b87c9a475514f0a1d5c8e2b11d7 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Mon, 15 Jul 2024 08:20:30 +0900 Subject: [PATCH 13/39] =?UTF-8?q?[feat]=20#44=20-=20=ED=99=88=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EA=B3=B5=EC=97=B0=20=EB=B0=8F=20=ED=99=8D?= =?UTF-8?q?=EB=B3=B4=20=EC=A1=B0=ED=9A=8C=20GET=20API=20=EA=B5=AC=ED=98=84?= =?UTF-8?q?=20(#47)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 --- .../performance/api/HomeController.java | 33 +++++++++ .../application/PerformanceService.java | 72 ++++++++++++++++++- .../dto/HomePerformanceDetail.java | 16 +++++ .../application/dto/HomePromotionDetail.java | 11 +++ .../application/dto/HomeRequest.java | 6 ++ .../application/dto/HomeResponse.java | 12 ++++ .../dao/PerformanceRepository.java | 5 ++ .../exception/PerformanceErrorCode.java | 8 ++- .../exception/PerformanceSuccessCode.java | 4 +- .../promotion/dao/PromotionRepository.java | 7 ++ .../domain/promotion/domain/Promotion.java | 14 ++-- .../schedule/application/ScheduleService.java | 30 +++++++- 12 files changed, 203 insertions(+), 15 deletions(-) create mode 100644 src/main/java/com/beat/domain/performance/api/HomeController.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java create mode 100644 src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java diff --git a/src/main/java/com/beat/domain/performance/api/HomeController.java b/src/main/java/com/beat/domain/performance/api/HomeController.java new file mode 100644 index 00000000..275bb45d --- /dev/null +++ b/src/main/java/com/beat/domain/performance/api/HomeController.java @@ -0,0 +1,33 @@ +package com.beat.domain.performance.api; + +import com.beat.domain.performance.application.PerformanceService; +import com.beat.domain.performance.application.dto.HomeRequest; +import com.beat.domain.performance.application.dto.HomeResponse; +import com.beat.domain.performance.domain.Genre; +import com.beat.domain.performance.exception.PerformanceSuccessCode; +import com.beat.global.common.dto.SuccessResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/main") +@RequiredArgsConstructor +public class HomeController { + + private final PerformanceService performanceService; + + @GetMapping + public ResponseEntity> getHomePerformanceList(@RequestParam(required = false) String genre) { + HomeRequest homeRequest; + if (genre != null) { + homeRequest = new HomeRequest(Genre.valueOf(genre)); + } else { + homeRequest = new HomeRequest(null); + } HomeResponse homeResponse = performanceService.getHomePerformanceList(homeRequest); + return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.HOME_PERFORMANCE_RETRIEVE_SUCCESS, homeResponse)); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceService.java b/src/main/java/com/beat/domain/performance/application/PerformanceService.java index 2ac48266..59770817 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -1,30 +1,32 @@ package com.beat.domain.performance.application; - import com.beat.domain.performance.application.dto.*; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; import com.beat.domain.performance.exception.PerformanceErrorCode; +import com.beat.domain.promotion.dao.PromotionRepository; +import com.beat.domain.promotion.domain.Promotion; import com.beat.domain.schedule.application.ScheduleService; import com.beat.domain.schedule.dao.ScheduleRepository; import com.beat.domain.cast.dao.CastRepository; +import com.beat.domain.schedule.domain.Schedule; import com.beat.domain.staff.dao.StaffRepository; import com.beat.global.common.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; - @Service @RequiredArgsConstructor public class PerformanceService { - private final PerformanceRepository performanceRepository; private final ScheduleRepository scheduleRepository; private final CastRepository castRepository; private final StaffRepository staffRepository; private final ScheduleService scheduleService; + private final PromotionRepository promotionRepository; @Transactional(readOnly = true) public PerformanceDetailResponse getPerformanceDetail(Long performanceId) { @@ -102,4 +104,68 @@ public BookingPerformanceDetailResponse getBookingPerformanceDetail(Long perform performance.getPerformanceTeamName() ); } + + @Transactional(readOnly = true) + public HomeResponse getHomePerformanceList(HomeRequest homeRequest) { + List performances; + + if (homeRequest.genre() != null) { + performances = performanceRepository.findByGenre(homeRequest.genre()); + } else { + performances = performanceRepository.findAll(); + } + + if (performances.isEmpty()) { + List promotions = getPromotions(); + return HomeResponse.of(promotions, new ArrayList<>()); + } + + List performanceDetails = performances.stream() + .map(performance -> { + List schedules = scheduleRepository.findByPerformanceId(performance.getId()); + int minDueDate = scheduleService.getMinDueDate(schedules); + + return HomePerformanceDetail.of( + performance.getId(), + performance.getPerformanceTitle(), + performance.getPerformancePeriod(), + performance.getTicketPrice(), + minDueDate, + performance.getGenre().name(), + performance.getPosterImage(), + performance.getPerformanceVenue() + ); + }) + .collect(Collectors.toList()); + + // 두 개의 스트림을 각각 처리하여 병합 + List positiveDueDates = performanceDetails.stream() + .filter(detail -> detail.dueDate() >= 0) + .sorted((p1, p2) -> Integer.compare(p1.dueDate(), p2.dueDate())) + .collect(Collectors.toList()); + + List negativeDueDates = performanceDetails.stream() + .filter(detail -> detail.dueDate() < 0) + .sorted((p1, p2) -> Integer.compare(p2.dueDate(), p1.dueDate())) + .collect(Collectors.toList()); + + // 병합된 리스트 + positiveDueDates.addAll(negativeDueDates); + + List promotions = getPromotions(); + + return HomeResponse.of(promotions, positiveDueDates); + } + + private List getPromotions() { + List promotionList = promotionRepository.findAll(); + return promotionList.stream() + .map(promotion -> HomePromotionDetail.of( + promotion.getId(), + promotion.getPromotionPhoto(), + promotion.getPerformance().getId() + )) + .collect(Collectors.toList()); + } + } diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java b/src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java new file mode 100644 index 00000000..51537a79 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java @@ -0,0 +1,16 @@ +package com.beat.domain.performance.application.dto; + +public record HomePerformanceDetail( + Long performanceId, + String performanceTitle, + String performancePeriod, + int ticketPrice, + int dueDate, + String genre, + String posterImage, + String performanceVenue +) { + public static HomePerformanceDetail of(Long performanceId, String performanceTitle, String performancePeriod, int ticketPrice, int dueDate, String genre, String posterImage, String performanceVenue) { + return new HomePerformanceDetail(performanceId, performanceTitle, performancePeriod, ticketPrice, dueDate, genre, posterImage, performanceVenue); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java b/src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java new file mode 100644 index 00000000..02ead354 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java @@ -0,0 +1,11 @@ +package com.beat.domain.performance.application.dto; + +public record HomePromotionDetail( + Long promotionId, + String promotionPhoto, + Long performanceId +) { + public static HomePromotionDetail of(Long promotionId, String promotionPhoto, Long performanceId) { + return new HomePromotionDetail(promotionId, promotionPhoto, performanceId); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java b/src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java new file mode 100644 index 00000000..518eae89 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java @@ -0,0 +1,6 @@ +package com.beat.domain.performance.application.dto; + +import com.beat.domain.performance.domain.Genre; + +public record HomeRequest(Genre genre) { +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java b/src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java new file mode 100644 index 00000000..7690d1c4 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto; + +import java.util.List; + +public record HomeResponse( + List promotionList, + List performanceList +) { + public static HomeResponse of(List promotionList, List performanceList) { + return new HomeResponse(promotionList, performanceList); + } +} diff --git a/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java index acb20231..ac4d6a37 100644 --- a/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java +++ b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java @@ -1,7 +1,12 @@ package com.beat.domain.performance.dao; +import com.beat.domain.performance.domain.Genre; import com.beat.domain.performance.domain.Performance; import org.springframework.data.jpa.repository.JpaRepository; +import java.util.List; + public interface PerformanceRepository extends JpaRepository { + List findByGenre(Genre genre); + } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java index fed3a9dc..9846a018 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java @@ -7,7 +7,13 @@ @Getter @RequiredArgsConstructor public enum PerformanceErrorCode implements BaseErrorCode { - PERFORMANCE_NOT_FOUND(404, "해당 공연 정보를 찾을 수 없습니다."); + PERFORMANCE_NOT_FOUND(404, "해당 공연 정보를 찾을 수 없습니다."), + REQUIRED_DATA_MISSING(400, "필수 데이터가 누락되었습니다."), + INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), + INVALID_REQUEST_FORMAT(400, "잘못된 요청 형식입니다."), + NO_PERFORMANCE_FOUND(404, "공연을 찾을 수 없습니다."), + INTERNAL_SERVER_ERROR(500, "서버 내부 오류입니다.") + ; private final int status; private final String message; diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java index 723a20a6..2f228eb9 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java @@ -8,7 +8,9 @@ @RequiredArgsConstructor public enum PerformanceSuccessCode implements BaseSuccessCode { PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."), - BOOKING_PERFORMANCE_RETRIEVE_SUCCESS(200, "예매 관련 공연 정보 조회가 성공적으로 완료되었습니다."); + BOOKING_PERFORMANCE_RETRIEVE_SUCCESS(200, "예매 관련 공연 정보 조회가 성공적으로 완료되었습니다."), + HOME_PERFORMANCE_RETRIEVE_SUCCESS(200, "홈 화면 공연 목록 조회가 성공적으로 완료되었습니다.") + ; private final int status; private final String message; diff --git a/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java b/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java new file mode 100644 index 00000000..95a64b0b --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java @@ -0,0 +1,7 @@ +package com.beat.domain.promotion.dao; + +import com.beat.domain.promotion.domain.Promotion; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PromotionRepository extends JpaRepository { +} diff --git a/src/main/java/com/beat/domain/promotion/domain/Promotion.java b/src/main/java/com/beat/domain/promotion/domain/Promotion.java index 077ba7d2..3cd7521f 100644 --- a/src/main/java/com/beat/domain/promotion/domain/Promotion.java +++ b/src/main/java/com/beat/domain/promotion/domain/Promotion.java @@ -1,10 +1,7 @@ package com.beat.domain.promotion.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import com.beat.domain.performance.domain.Performance; +import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; @@ -16,11 +13,12 @@ public class Promotion { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long promotionId; + private Long id; @Column(nullable = false) private String promotionPhoto; - @Column(nullable = false) - private Long performanceId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = false) + private Performance performance; } diff --git a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java index dddbc949..a8162ce6 100644 --- a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java +++ b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java @@ -1,5 +1,4 @@ package com.beat.domain.schedule.application; - import com.beat.domain.schedule.application.dto.TicketAvailabilityRequest; import com.beat.domain.schedule.application.dto.TicketAvailabilityResponse; import com.beat.domain.schedule.dao.ScheduleRepository; @@ -12,7 +11,11 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.OptionalInt; @Service @RequiredArgsConstructor @@ -68,4 +71,27 @@ public void updateBookingStatus(Schedule schedule) { scheduleRepository.save(schedule); } } -} \ No newline at end of file + + public int calculateDueDate(Schedule schedule) { + // LocalDate 객체를 사용하여 날짜 차이만 계산 + int dueDate = (int) ChronoUnit.DAYS.between(LocalDate.now(), schedule.getPerformanceDate().toLocalDate()); + return dueDate; + } + + public int getMinDueDate(List schedules) { + OptionalInt minPositiveDueDate = schedules.stream() + .mapToInt(this::calculateDueDate) + .filter(dueDate -> dueDate >= 0) + .min(); + + if (minPositiveDueDate.isPresent()) { + return minPositiveDueDate.getAsInt(); + } else { + return schedules.stream() + .mapToInt(this::calculateDueDate) + .min() + .orElse(Integer.MAX_VALUE); + } + } + +} From 6f28c55e480135d6c8ca350a34cfc55189581ddd Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Mon, 15 Jul 2024 08:20:52 +0900 Subject: [PATCH 14/39] =?UTF-8?q?[feat]=20#46=20-=20=ED=9A=8C=EC=9B=90=20?= =?UTF-8?q?=EC=98=88=EB=A7=A4=20=EC=A1=B0=ED=9A=8C=20GET=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#48)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 --- .../domain/booking/api/BookingController.java | 36 +++++++--- ....java => GuestBookingRetrieveService.java} | 28 ++++---- .../MemberBookingRetrieveService.java | 65 +++++++++++++++++++ .../dto/BookingRetrieveRequest.java | 12 ---- .../dto/GuestBookingRetrieveRequest.java | 12 ++++ ...java => GuestBookingRetrieveResponse.java} | 6 +- .../dto/MemberBookingRetrieveResponse.java | 56 ++++++++++++++++ .../domain/booking/dao/BookingRepository.java | 2 + .../booking/exception/BookingSuccessCode.java | 3 +- .../global/common/config/SecurityConfig.java | 5 +- 10 files changed, 182 insertions(+), 43 deletions(-) rename src/main/java/com/beat/domain/booking/application/{BookingRetrieveService.java => GuestBookingRetrieveService.java} (60%) create mode 100644 src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java delete mode 100644 src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveRequest.java rename src/main/java/com/beat/domain/booking/application/dto/{BookingRetrieveResponse.java => GuestBookingRetrieveResponse.java} (90%) create mode 100644 src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java index 76d6ba7e..975f18dc 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -1,20 +1,24 @@ package com.beat.domain.booking.api; -import com.beat.domain.booking.application.BookingRetrieveService; +import com.beat.domain.booking.application.GuestBookingRetrieveService; import com.beat.domain.booking.application.GuestBookingService; +import com.beat.domain.booking.application.MemberBookingRetrieveService; import com.beat.domain.booking.application.MemberBookingService; -import com.beat.domain.booking.application.dto.BookingRetrieveRequest; -import com.beat.domain.booking.application.dto.BookingRetrieveResponse; +import com.beat.domain.booking.application.dto.GuestBookingRetrieveRequest; +import com.beat.domain.booking.application.dto.GuestBookingRetrieveResponse; import com.beat.domain.booking.application.dto.GuestBookingRequest; import com.beat.domain.booking.application.dto.GuestBookingResponse; import com.beat.domain.booking.application.dto.MemberBookingRequest; import com.beat.domain.booking.application.dto.MemberBookingResponse; +import com.beat.domain.booking.application.dto.MemberBookingRetrieveResponse; import com.beat.domain.booking.exception.BookingSuccessCode; import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; @@ -26,11 +30,12 @@ @RequestMapping("/api/bookings") @RequiredArgsConstructor public class BookingController { - - private final BookingRetrieveService bookingRetrieveService; - private final GuestBookingService guestBookingService; private final MemberBookingService memberBookingService; + private final MemberBookingRetrieveService memberBookingRetrieveService; + private final GuestBookingService guestBookingService; + private final GuestBookingRetrieveService guestBookingRetrieveService; + @Operation(summary = "회원 예매 API", description = "회원이 예매를 요청하는 POST API입니다.") @PostMapping("/member") public ResponseEntity> createMemberBooking( @CurrentMember Long memberId, @@ -40,6 +45,16 @@ public ResponseEntity> createMemberBookin .body(SuccessResponse.of(BookingSuccessCode.MEMBER_BOOKING_SUCCESS, response)); } + @Operation(summary = "회원 예매 조회 API", description = "회원이 예매를 조회하는 GET API입니다.") + @GetMapping("/member/retrieve") + public ResponseEntity> getMemberBookings( + @CurrentMember Long memberId) { + List response = memberBookingRetrieveService.findMemberBookings(memberId); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(BookingSuccessCode.MEMBER_BOOKING_SUCCESS, response)); + } + + @Operation(summary = "회원 예매 API", description = "비회원이 예매를 요청하는 POST API입니다.") @PostMapping("/guest") public ResponseEntity> createGuestBookings( @RequestBody GuestBookingRequest guestBookingRequest) { @@ -48,11 +63,12 @@ public ResponseEntity> createGuestBookings .body(SuccessResponse.of(BookingSuccessCode.GUEST_BOOKING_SUCCESS, response)); } + @Operation(summary = "비회원 예매 조회 API", description = "비회원이 예매를 조회하는 POST API입니다.") @PostMapping("/guest/retrieve") - public ResponseEntity>> getGuestBookings( - @RequestBody BookingRetrieveRequest bookingRetrieveRequest) { - List response = bookingRetrieveService.findGuestBookings(bookingRetrieveRequest); + public ResponseEntity>> getGuestBookings( + @RequestBody GuestBookingRetrieveRequest guestBookingRetrieveRequest) { + List response = guestBookingRetrieveService.findGuestBookings(guestBookingRetrieveRequest); return ResponseEntity.status(HttpStatus.OK) - .body(SuccessResponse.of(BookingSuccessCode.BOOKING_RETRIEVE_SUCCESS, response)); + .body(SuccessResponse.of(BookingSuccessCode.GUEST_BOOKING_RETRIEVE_SUCCESS, response)); } } diff --git a/src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java b/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java similarity index 60% rename from src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java rename to src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java index 2a9f401d..de25cd47 100644 --- a/src/main/java/com/beat/domain/booking/application/BookingRetrieveService.java +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java @@ -1,7 +1,7 @@ package com.beat.domain.booking.application; -import com.beat.domain.booking.application.dto.BookingRetrieveRequest; -import com.beat.domain.booking.application.dto.BookingRetrieveResponse; +import com.beat.domain.booking.application.dto.GuestBookingRetrieveRequest; +import com.beat.domain.booking.application.dto.GuestBookingRetrieveResponse; import com.beat.domain.booking.dao.BookingRepository; import com.beat.domain.booking.domain.Booking; import com.beat.domain.booking.exception.BookingErrorCode; @@ -17,16 +17,16 @@ @Service @RequiredArgsConstructor -public class BookingRetrieveService { +public class GuestBookingRetrieveService { private final BookingRepository bookingRepository; - public List findGuestBookings(BookingRetrieveRequest bookingRetrieveRequest) { + public List findGuestBookings(GuestBookingRetrieveRequest guestBookingRetrieveRequest) { - validateRequest(bookingRetrieveRequest); + validateRequest(guestBookingRetrieveRequest); List bookings = bookingRepository.findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthDate( - bookingRetrieveRequest.bookerName(), bookingRetrieveRequest.bookerPhoneNumber(), bookingRetrieveRequest.password(), bookingRetrieveRequest.birthDate()).orElseThrow( + guestBookingRetrieveRequest.bookerName(), guestBookingRetrieveRequest.bookerPhoneNumber(), guestBookingRetrieveRequest.password(), guestBookingRetrieveRequest.birthDate()).orElseThrow( () -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); if (bookings.isEmpty()) { @@ -38,33 +38,33 @@ public List findGuestBookings(BookingRetrieveRequest bo .toList(); } - private void validateRequest(BookingRetrieveRequest bookingRetrieveRequest) { - if (bookingRetrieveRequest.bookerName() == null || bookingRetrieveRequest.bookerPhoneNumber() == null || bookingRetrieveRequest.password() == null || bookingRetrieveRequest.birthDate() == null) { + private void validateRequest(GuestBookingRetrieveRequest guestBookingRetrieveRequest) { + if (guestBookingRetrieveRequest.bookerName() == null || guestBookingRetrieveRequest.bookerPhoneNumber() == null || guestBookingRetrieveRequest.password() == null || guestBookingRetrieveRequest.birthDate() == null) { throw new BadRequestException(BookingErrorCode.REQUIRED_DATA_MISSING); } - if (!Pattern.matches("^[a-zA-Z가-힣]+$", bookingRetrieveRequest.bookerName())) { // 예매자 이름은 알파벳, 한글 형식 + if (!Pattern.matches("^[a-zA-Z가-힣]+$", guestBookingRetrieveRequest.bookerName())) { // 예매자 이름은 알파벳, 한글 형식 throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); } - if (!Pattern.matches("^\\d{3}-\\d{4}-\\d{4}$", bookingRetrieveRequest.bookerPhoneNumber())) { // 전화번호는 010-1234-5678 형식 + if (!Pattern.matches("^\\d{3}-\\d{4}-\\d{4}$", guestBookingRetrieveRequest.bookerPhoneNumber())) { // 전화번호는 010-1234-5678 형식 throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); } - if (!Pattern.matches("^\\d{4}$", bookingRetrieveRequest.password())) { // 비밀번호는 4자리 숫자 형식 + if (!Pattern.matches("^\\d{4}$", guestBookingRetrieveRequest.password())) { // 비밀번호는 4자리 숫자 형식 throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); } - if (!Pattern.matches("^\\d{6}$", bookingRetrieveRequest.birthDate())) { // 생년월일은 6자리 숫자 형식 + if (!Pattern.matches("^\\d{6}$", guestBookingRetrieveRequest.birthDate())) { // 생년월일은 6자리 숫자 형식 throw new BadRequestException(BookingErrorCode.INVALID_REQUEST_FORMAT); } } - private BookingRetrieveResponse toBookingResponse(Booking booking) { + private GuestBookingRetrieveResponse toBookingResponse(Booking booking) { Schedule schedule = booking.getSchedule(); Performance performance = schedule.getPerformance(); - return BookingRetrieveResponse.of( + return GuestBookingRetrieveResponse.of( booking.getId(), schedule.getId(), performance.getPerformanceTitle(), diff --git a/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java b/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java new file mode 100644 index 00000000..17d4340a --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java @@ -0,0 +1,65 @@ +package com.beat.domain.booking.application; + +import com.beat.domain.booking.application.dto.MemberBookingRetrieveResponse; +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.exception.MemberErrorCode; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class MemberBookingRetrieveService { + + private final BookingRepository bookingRepository; + private final MemberRepository memberRepository; + private final UserRepository userRepository; + + public List findMemberBookings(Long memberId) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + List bookings = bookingRepository.findByUsersId(user.getId()); + + return bookings.stream() + .map(this::toMemberBookingResponse) + .collect(Collectors.toList()); + } + + private MemberBookingRetrieveResponse toMemberBookingResponse(Booking booking) { + Schedule schedule = booking.getSchedule(); + Performance performance = schedule.getPerformance(); + + return MemberBookingRetrieveResponse.of( + booking.getUsers().getId(), + booking.getId(), + schedule.getId(), + performance.getPerformanceTitle(), + schedule.getPerformanceDate(), + performance.getPerformanceVenue(), + booking.getPurchaseTicketCount(), + schedule.getScheduleNumber().name(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + performance.getBankName().name(), + performance.getAccountNumber(), + schedule.getId().intValue(), + booking.isPaymentCompleted(), + booking.getCreatedAt() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java b/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java deleted file mode 100644 index 0a63e70b..00000000 --- a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveRequest.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.beat.domain.booking.application.dto; - -public record BookingRetrieveRequest( - String bookerName, - String birthDate, - String bookerPhoneNumber, - String password -) { - public static BookingRetrieveRequest of(String bookerName, String birthDate, String bookerPhoneNumber, String password) { - return new BookingRetrieveRequest(bookerName, birthDate, bookerPhoneNumber, password); - } -} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveRequest.java b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveRequest.java new file mode 100644 index 00000000..d0703522 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveRequest.java @@ -0,0 +1,12 @@ +package com.beat.domain.booking.application.dto; + +public record GuestBookingRetrieveRequest( + String bookerName, + String birthDate, + String bookerPhoneNumber, + String password +) { +// public static GuestBookingRetrieveRequest of(String bookerName, String birthDate, String bookerPhoneNumber, String password) { +// return new GuestBookingRetrieveRequest(bookerName, birthDate, bookerPhoneNumber, password); +// } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java similarity index 90% rename from src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java rename to src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java index c155f178..49de4e08 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/BookingRetrieveResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java @@ -2,7 +2,7 @@ import java.time.LocalDateTime; -public record BookingRetrieveResponse( +public record GuestBookingRetrieveResponse( Long bookingId, Long scheduleId, String performanceTitle, @@ -18,7 +18,7 @@ public record BookingRetrieveResponse( boolean isPaymentCompleted, LocalDateTime createdAt ) { - public static BookingRetrieveResponse of( + public static GuestBookingRetrieveResponse of( Long bookingId, Long scheduleId, String performanceTitle, @@ -34,7 +34,7 @@ public static BookingRetrieveResponse of( boolean isPaymentCompleted, LocalDateTime createdAt ) { - return new BookingRetrieveResponse( + return new GuestBookingRetrieveResponse( bookingId, scheduleId, performanceTitle, diff --git a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java new file mode 100644 index 00000000..9e23a7d2 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java @@ -0,0 +1,56 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record MemberBookingRetrieveResponse( + Long userId, + Long bookingId, + Long scheduleId, + String performanceTitle, + LocalDateTime performanceDate, + String performanceVenue, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + String bankName, + String accountNumber, + int dueDate, + boolean isPaymentCompleted, + LocalDateTime createdAt +) { + public static MemberBookingRetrieveResponse of( + Long userId, + Long bookingId, + Long scheduleId, + String performanceTitle, + LocalDateTime performanceDate, + String performanceVenue, + int purchaseTicketCount, + String scheduleNumber, + String bookerName, + String bookerPhoneNumber, + String bankName, + String accountNumber, + int dueDate, + boolean isPaymentCompleted, + LocalDateTime createdAt) { + return new MemberBookingRetrieveResponse( + userId, + bookingId, + scheduleId, + performanceTitle, + performanceDate, + performanceVenue, + purchaseTicketCount, + scheduleNumber, + bookerName, + bookerPhoneNumber, + bankName, + accountNumber, + dueDate, + isPaymentCompleted, + createdAt + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java index 89bdc5da..8c60d54f 100644 --- a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java +++ b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java @@ -20,4 +20,6 @@ Optional findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPasswo String birthDate, String password ); + + List findByUsersId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java index 9fc1a7f8..c8917451 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -9,7 +9,8 @@ public enum BookingSuccessCode implements BaseSuccessCode { MEMBER_BOOKING_SUCCESS(201, "회원 예매가 성공적으로 완료되었습니다"), GUEST_BOOKING_SUCCESS(201, "비회원 예매가 성공적으로 완료되었습니다"), - BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") + MEMBER_BOOKING_RETRIEVE_SUCCESS(200, "회원 예매 조회가 성공적으로 완료되었습니다."), + GUEST_BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") ; private final int status; diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 0443cb7c..61f9d879 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -3,17 +3,14 @@ import com.beat.global.auth.jwt.filter.JwtAuthenticationFilter; import com.beat.global.auth.security.CustomAccessDeniedHandler; import com.beat.global.auth.security.CustomJwtAuthenticationEntryPoint; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; -import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @EnableWebSecurity @@ -34,6 +31,8 @@ public class SecurityConfig { "/api/v1/swagger-resources/**", "/api/performances/detail/**", "/api/performances/booking/**", + "/api/bookings/guest/**", + "/api/schedules/**" // "/login/oauth2/code/kakao", // "/kakao/callback" }; From b90138d67c7c5a2e6f59ddcec82eb7b80f1c6b86 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Mon, 15 Jul 2024 20:43:19 +0900 Subject: [PATCH 15/39] =?UTF-8?q?[feat]=20#49=20-=20=EC=98=88=EB=A7=A4?= =?UTF-8?q?=EC=9E=90=20=EA=B4=80=EB=A6=AC=20API=20=EA=B5=AC=ED=98=84=20(#5?= =?UTF-8?q?1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 --- .../domain/booking/api/TicketController.java | 53 ++++++++ .../booking/application/TicketService.java | 117 ++++++++++++++++++ .../application/dto/TicketDeleteRequest.java | 12 ++ .../booking/application/dto/TicketDetail.java | 26 ++++ .../dto/TicketRetrieveResponse.java | 16 ++- .../application/dto/TicketUpdateDetail.java | 26 ++++ .../application/dto/TicketUpdateRequest.java | 19 +++ .../domain/booking/dao/TicketRepository.java | 17 ++- .../beat/domain/booking/domain/Booking.java | 5 + .../booking/exception/BookingErrorCode.java | 5 +- .../booking/exception/BookingSuccessCode.java | 5 +- .../domain/member/api/MemberController.java | 4 + .../performance/api/HomeController.java | 2 + .../api/PerformanceController.java | 3 + .../auth/client/kakao/KakaoApiClient.java | 2 + .../auth/client/kakao/KakaoAuthApiClient.java | 3 + 16 files changed, 311 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/beat/domain/booking/api/TicketController.java create mode 100644 src/main/java/com/beat/domain/booking/application/TicketService.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java create mode 100644 src/main/java/com/beat/domain/booking/application/dto/TicketUpdateRequest.java diff --git a/src/main/java/com/beat/domain/booking/api/TicketController.java b/src/main/java/com/beat/domain/booking/api/TicketController.java new file mode 100644 index 00000000..75d9973e --- /dev/null +++ b/src/main/java/com/beat/domain/booking/api/TicketController.java @@ -0,0 +1,53 @@ +package com.beat.domain.booking.api; + +import com.beat.domain.booking.application.TicketService; +import com.beat.domain.booking.application.dto.TicketDeleteRequest; +import com.beat.domain.booking.application.dto.TicketRetrieveResponse; +import com.beat.domain.booking.application.dto.TicketUpdateRequest; +import com.beat.domain.booking.exception.BookingSuccessCode; +import com.beat.global.auth.annotation.CurrentMember; +import com.beat.global.common.dto.SuccessResponse; +import com.beat.domain.schedule.domain.ScheduleNumber; +import io.swagger.v3.oas.annotations.Operation; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/api/tickets") +@RequiredArgsConstructor +public class TicketController { + + private final TicketService ticketService; + + @Operation(summary = "예매자 목록 조회 API", description = "메이커가 자신의 공연에 대한 예매자 목록을 조회하는 GET API입니다.") + @GetMapping("/{performanceId}") + public ResponseEntity> getTickets( + @CurrentMember Long memberId, + @PathVariable Long performanceId, + @RequestParam(required = false) ScheduleNumber scheduleNumber, + @RequestParam(required = false) Boolean isPaymentCompleted) { + TicketRetrieveResponse response = ticketService.getTickets(memberId, performanceId, scheduleNumber, isPaymentCompleted); + return ResponseEntity.ok(SuccessResponse.of(BookingSuccessCode.TICKET_RETRIEVE_SUCCESS, response)); + } + + @Operation(summary = "예매자 입금여부 수정 API", description = "메이커가 자신의 공연에 대한 예매자의 입금여부 정보를 수정하는 PUT API입니다.") + @PutMapping("/{performanceId}") + public ResponseEntity> updateTickets( + @CurrentMember Long memberId, + @PathVariable Long performanceId, + @RequestBody TicketUpdateRequest request) { + ticketService.updateTickets(memberId, request); + return ResponseEntity.ok(SuccessResponse.of(BookingSuccessCode.TICKET_UPDATE_SUCCESS, null)); + } + + @Operation(summary = "예매자 삭제 API", description = "메이커가 자신의 공연에 대한 예매자의 정보를 삭제하는 DELETE API입니다.") + @DeleteMapping("/{performanceId}") + public ResponseEntity> deleteTickets( + @CurrentMember Long memberId, + @PathVariable Long performanceId, + @RequestBody TicketDeleteRequest request) { + ticketService.deleteTickets(memberId, request); + return ResponseEntity.ok(SuccessResponse.of(BookingSuccessCode.TICKET_DELETE_SUCCESS, null)); + } +} diff --git a/src/main/java/com/beat/domain/booking/application/TicketService.java b/src/main/java/com/beat/domain/booking/application/TicketService.java new file mode 100644 index 00000000..0918f02e --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/TicketService.java @@ -0,0 +1,117 @@ +package com.beat.domain.booking.application; + +import com.beat.domain.booking.application.dto.*; +import com.beat.domain.booking.dao.TicketRepository; +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.exception.MemberErrorCode; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.schedule.domain.ScheduleNumber; +import com.beat.domain.booking.exception.BookingErrorCode; +import com.beat.global.common.exception.NotFoundException; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class TicketService { + + private final TicketRepository ticketRepository; + private final PerformanceRepository performanceRepository; + private final MemberRepository memberRepository; + private final UserRepository userRepository; + + public TicketRetrieveResponse getTickets(Long memberId, Long performanceId, ScheduleNumber scheduleNumber, Boolean isPaymentCompleted) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + Performance performance = performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + + List bookings; + + if (scheduleNumber != null && isPaymentCompleted != null) { + bookings = ticketRepository.findBySchedulePerformanceIdAndScheduleScheduleNumberAndIsPaymentCompleted(performanceId, scheduleNumber, isPaymentCompleted); + } else if (scheduleNumber != null) { + bookings = ticketRepository.findBySchedulePerformanceIdAndScheduleScheduleNumber(performanceId, scheduleNumber); + } else if (isPaymentCompleted != null) { + bookings = ticketRepository.findBySchedulePerformanceIdAndIsPaymentCompleted(performanceId, isPaymentCompleted); + } else { + bookings = ticketRepository.findBySchedulePerformanceId(performanceId); + } + + if (bookings.isEmpty()) { + throw new NotFoundException(BookingErrorCode.NO_TICKETS_FOUND); + } + + List bookingList = bookings.stream() + .map(booking -> TicketDetail.of( + booking.getId(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + booking.getSchedule().getId(), + booking.getPurchaseTicketCount(), + booking.getCreatedAt(), + booking.isPaymentCompleted(), + booking.getSchedule().getScheduleNumber().name() + )) + .collect(Collectors.toList()); + + return TicketRetrieveResponse.of( + performance.getPerformanceTitle(), + performance.getTotalScheduleCount(), + bookingList + ); + } + + @Transactional + public void updateTickets(Long memberId, TicketUpdateRequest request) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + Performance performance = performanceRepository.findById(request.performanceId()) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_PERFORMANCE_FOUND)); + + for (TicketUpdateDetail detail : request.bookingList()) { + Booking booking = ticketRepository.findById(detail.bookingId()) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + + booking.setIsPaymentCompleted(detail.isPaymentCompleted()); + ticketRepository.save(booking); + } + } + + @Transactional + public void deleteTickets(Long memberId, TicketDeleteRequest request) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + Performance performance = performanceRepository.findById(request.performanceId()) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_PERFORMANCE_FOUND)); + + for (Long bookingId : request.bookingList()) { + Booking booking = ticketRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + + ticketRepository.delete(booking); + } + } +} diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java b/src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java new file mode 100644 index 00000000..57c2e756 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java @@ -0,0 +1,12 @@ +package com.beat.domain.booking.application.dto; + +import java.util.List; + +public record TicketDeleteRequest( + Long performanceId, + List bookingList +) { + public static TicketDeleteRequest of(Long performanceId, List bookingList) { + return new TicketDeleteRequest(performanceId, bookingList); + } +} diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java b/src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java new file mode 100644 index 00000000..013eb32c --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java @@ -0,0 +1,26 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record TicketDetail( + Long bookingId, + String bookerName, + String bookerPhoneNumber, + Long scheduleId, + int purchaseTicketCount, + LocalDateTime createdAt, + boolean isPaymentCompleted, + String scheduleNumber +) { + public static TicketDetail of( + Long bookingId, + String bookerName, + String bookerPhoneNumber, + Long scheduleId, + int purchaseTicketCount, + LocalDateTime createdAt, + boolean isPaymentCompleted, + String scheduleNumber) { + return new TicketDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, isPaymentCompleted, scheduleNumber); + } +} diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java index db46a030..0041ec3e 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketRetrieveResponse.java @@ -1,2 +1,16 @@ -package com.beat.domain.booking.application.dto;public record TicketRetrieveResponse() { +package com.beat.domain.booking.application.dto; + +import java.util.List; + +public record TicketRetrieveResponse( + String performanceTitle, + int totalScheduleCount, + List bookingList +) { + public static TicketRetrieveResponse of( + String performanceTitle, + int totalScheduleCount, + List bookingList) { + return new TicketRetrieveResponse(performanceTitle, totalScheduleCount, bookingList); + } } diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java b/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java new file mode 100644 index 00000000..38e868df --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java @@ -0,0 +1,26 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; + +public record TicketUpdateDetail( + Long bookingId, + String bookerName, + String bookerPhoneNumber, + Long scheduleId, + int purchaseTicketCount, + LocalDateTime createdAt, + boolean isPaymentCompleted, + String scheduleNumber +) { + public static TicketUpdateDetail of( + Long bookingId, + String bookerName, + String bookerPhoneNumber, + Long scheduleId, + int purchaseTicketCount, + LocalDateTime createdAt, + boolean isPaymentCompleted, + String scheduleNumber) { + return new TicketUpdateDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, isPaymentCompleted, scheduleNumber); + } +} diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateRequest.java b/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateRequest.java new file mode 100644 index 00000000..5395b1d7 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateRequest.java @@ -0,0 +1,19 @@ +package com.beat.domain.booking.application.dto; + +import java.time.LocalDateTime; +import java.util.List; + +public record TicketUpdateRequest( + Long performanceId, + String performanceTitle, + int totalScheduleCount, + List bookingList +) { + public static TicketUpdateRequest of( + Long performanceId, + String performanceTitle, + int totalScheduleCount, + List bookingList) { + return new TicketUpdateRequest(performanceId, performanceTitle, totalScheduleCount, bookingList); + } +} diff --git a/src/main/java/com/beat/domain/booking/dao/TicketRepository.java b/src/main/java/com/beat/domain/booking/dao/TicketRepository.java index 2c134c0c..aad02e7e 100644 --- a/src/main/java/com/beat/domain/booking/dao/TicketRepository.java +++ b/src/main/java/com/beat/domain/booking/dao/TicketRepository.java @@ -1,2 +1,17 @@ -package com.beat.domain.booking.dao;public interface TicketRepository { +package com.beat.domain.booking.dao; + +import com.beat.domain.booking.domain.Booking; +import com.beat.domain.schedule.domain.ScheduleNumber; +import org.springframework.data.jpa.repository.JpaRepository; + +import java.util.List; + +public interface TicketRepository extends JpaRepository { + List findBySchedulePerformanceId(Long performanceId); + + List findBySchedulePerformanceIdAndScheduleScheduleNumber(Long performanceId, ScheduleNumber scheduleNumber); + + List findBySchedulePerformanceIdAndIsPaymentCompleted(Long performanceId, boolean isPaymentCompleted); + + List findBySchedulePerformanceIdAndScheduleScheduleNumberAndIsPaymentCompleted(Long performanceId, ScheduleNumber scheduleNumber, boolean isPaymentCompleted); } diff --git a/src/main/java/com/beat/domain/booking/domain/Booking.java b/src/main/java/com/beat/domain/booking/domain/Booking.java index 16487a74..39fba2fe 100644 --- a/src/main/java/com/beat/domain/booking/domain/Booking.java +++ b/src/main/java/com/beat/domain/booking/domain/Booking.java @@ -83,4 +83,9 @@ public static Booking create(int purchaseTicketCount, String bookerName, String .users(users) .build(); } + + public void setIsPaymentCompleted(boolean isPaymentCompleted) { + this.isPaymentCompleted = isPaymentCompleted; + } + } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java b/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java index b1ca314e..61dca7f6 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingErrorCode.java @@ -10,7 +10,10 @@ public enum BookingErrorCode implements BaseErrorCode { REQUIRED_DATA_MISSING(400, "필수 데이터가 누락되었습니다."), INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), INVALID_REQUEST_FORMAT(400, "잘못된 요청 형식입니다."), - NO_BOOKING_FOUND(404, "입력하신 정보와 일치하는 예매 내역이 없습니다. 확인 후 다시 조회해주세요.") + NO_BOOKING_FOUND(404, "입력하신 정보와 일치하는 예매 내역이 없습니다. 확인 후 다시 조회해주세요."), + NO_TICKETS_FOUND(404, "입력하신 정보와 일치하는 예매자 목록이 없습니다."), + NO_PERFORMANCE_FOUND(404, "공연을 찾을 수 없습니다."), + NO_SCHEDULE_FOUND(404, "회차를 찾을 수 없습니다.") ; private final int status; diff --git a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java index c8917451..e77708ff 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -10,7 +10,10 @@ public enum BookingSuccessCode implements BaseSuccessCode { MEMBER_BOOKING_SUCCESS(201, "회원 예매가 성공적으로 완료되었습니다"), GUEST_BOOKING_SUCCESS(201, "비회원 예매가 성공적으로 완료되었습니다"), MEMBER_BOOKING_RETRIEVE_SUCCESS(200, "회원 예매 조회가 성공적으로 완료되었습니다."), - GUEST_BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다.") + GUEST_BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다."), + TICKET_RETRIEVE_SUCCESS(200, "예매자 목록 조회가 성공적으로 완료되었습니다."), + TICKET_UPDATE_SUCCESS(200, "예매자 입금여부 수정이 성공적으로 완료되었습니다."), + TICKET_DELETE_SUCCESS(200, "예매자 삭제 요청이 성공했습니다.") ; private final int status; diff --git a/src/main/java/com/beat/domain/member/api/MemberController.java b/src/main/java/com/beat/domain/member/api/MemberController.java index 6ec5c406..08c06b30 100644 --- a/src/main/java/com/beat/domain/member/api/MemberController.java +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -6,6 +6,7 @@ import com.beat.global.auth.client.dto.MemberLoginRequest; import com.beat.global.auth.jwt.application.TokenService; import com.beat.global.common.dto.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; @@ -24,6 +25,7 @@ public class MemberController { private final static int COOKIE_MAX_AGE = 7 * 24 * 60 * 60; private final static String REFRESH_TOKEN = "refreshToken"; + @Operation(summary = "로그인/회원가입 API", description = "로그인/회원가입하는 POST API입니다.") @PostMapping("/sign-up") public ResponseEntity> signUp( @RequestParam final String authorizationCode, @@ -43,6 +45,7 @@ public ResponseEntity> signUp( .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, LoginSuccessResponse.of(loginSuccessResponse.accessToken(), null, loginSuccessResponse.nickname()))); } + @Operation(summary = "access token 재발급 API", description = "refresh token으로 access token을 재발급하는 GET API입니다.") @GetMapping("/refresh-token") public ResponseEntity> refreshToken( @RequestParam final String refreshToken @@ -52,6 +55,7 @@ public ResponseEntity> refreshToken( .body(SuccessResponse.of(MemberSuccessCode.ISSUE_REFRESH_TOKEN_SUCCESS, accessTokenGetSuccess)); } + @Operation(summary = "로그아웃 API", description = "로그아웃하는 POST API입니다.") @PostMapping("/sign-out") public ResponseEntity> signOut( final Principal principal diff --git a/src/main/java/com/beat/domain/performance/api/HomeController.java b/src/main/java/com/beat/domain/performance/api/HomeController.java index 275bb45d..9bb7abe2 100644 --- a/src/main/java/com/beat/domain/performance/api/HomeController.java +++ b/src/main/java/com/beat/domain/performance/api/HomeController.java @@ -6,6 +6,7 @@ import com.beat.domain.performance.domain.Genre; import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.global.common.dto.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -20,6 +21,7 @@ public class HomeController { private final PerformanceService performanceService; + @Operation(summary = "전체공연목록, 홍보목록 조회 API", description = "홈화면에서 전체공연목록, 홍보목록을 조회하는 GET API입니다.") @GetMapping public ResponseEntity> getHomePerformanceList(@RequestParam(required = false) String genre) { HomeRequest homeRequest; diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceController.java b/src/main/java/com/beat/domain/performance/api/PerformanceController.java index 72c14749..9d501e57 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceController.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -5,6 +5,7 @@ import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.domain.performance.application.PerformanceService; import com.beat.global.common.dto.SuccessResponse; +import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -19,6 +20,7 @@ public class PerformanceController { private final PerformanceService performanceService; + @Operation(summary = "공연 상세정보 조회 API", description = "공연 상세페이지의 공연 상세정보를 조회하는 GET API입니다.") @GetMapping("/detail/{performanceId}") public ResponseEntity> getPerformanceDetail( @PathVariable Long performanceId) { @@ -26,6 +28,7 @@ public ResponseEntity> getPerformance return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_RETRIEVE_SUCCESS, performanceDetail)); } + @Operation(summary = "예매하기 관련 공연 정보 조회 API", description = "예매하기 페이지에서 필요한 예매 관련 공연 정보를 조회하는 GET API입니다.") @GetMapping("/booking/{performanceId}") public ResponseEntity> getBookingPerformanceDetail( @PathVariable Long performanceId) { diff --git a/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java b/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java index d822e38b..a92205ef 100644 --- a/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java +++ b/src/main/java/com/beat/global/auth/client/kakao/KakaoApiClient.java @@ -1,6 +1,7 @@ package com.beat.global.auth.client.kakao; import com.beat.global.auth.client.kakao.response.KakaoUserResponse; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.HttpHeaders; import org.springframework.web.bind.annotation.GetMapping; @@ -9,6 +10,7 @@ @FeignClient(name = "kakaoApiClient", url = "https://kapi.kakao.com") public interface KakaoApiClient { + @Operation(summary = "카카오 개인정보 조회 API", description = "카카오로그인 결과 카카오 개인정보를 조회하는 GET API입니다.") @GetMapping(value = "/v2/user/me") KakaoUserResponse getUserInformation(@RequestHeader(HttpHeaders.AUTHORIZATION) String accessToken); } \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java b/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java index 7053944f..71b69567 100644 --- a/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java +++ b/src/main/java/com/beat/global/auth/client/kakao/KakaoAuthApiClient.java @@ -1,6 +1,7 @@ package com.beat.global.auth.client.kakao; import com.beat.global.auth.client.kakao.response.KakaoAccessTokenResponse; +import io.swagger.v3.oas.annotations.Operation; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PostMapping; @@ -8,6 +9,8 @@ @FeignClient(name = "kakaoAuthApiClient", url = "https://kauth.kakao.com") public interface KakaoAuthApiClient { + + @Operation(summary = "authorization code 발급 API", description = "토큰 발급에 필요한 authorization code를 발급하는 POST API입니다.") @PostMapping(value = "/oauth/token", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE) KakaoAccessTokenResponse getOAuth2AccessToken( @RequestParam("grant_type") String grantType, From f37c9e3e5536bcd97c0925f14ee1905737cfd942 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 01:04:48 +0900 Subject: [PATCH 16/39] =?UTF-8?q?HOTFIX(SecurityConfig):=20WHITELIST=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=EB=AA=85=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/beat/global/common/config/SecurityConfig.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 61f9d879..aebdb3c0 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -25,10 +25,10 @@ public class SecurityConfig { "/api/users/sign-up", "/api/users/refresh-token", "/api/users/sign-out", - "/api/v1/actuator/health", - "/api/v1/v3/api-docs/**", - "/api/v1/swagger-ui/**", - "/api/v1/swagger-resources/**", + "/api/actuator/health", + "/api/v3/api-docs/**", + "/api/swagger-ui/**", + "/api/swagger-resources/**", "/api/performances/detail/**", "/api/performances/booking/**", "/api/bookings/guest/**", From 3ab27650b786e20402dc78ba6104855bc91d5b41 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 01:30:44 +0900 Subject: [PATCH 17/39] =?UTF-8?q?[fix]=20#52=20-=20auth=20whitelist=20?= =?UTF-8?q?=EC=88=98=EC=A0=95=20(#53)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> --- .../com/beat/global/common/config/SecurityConfig.java | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index aebdb3c0..4204ba13 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -25,16 +25,14 @@ public class SecurityConfig { "/api/users/sign-up", "/api/users/refresh-token", "/api/users/sign-out", - "/api/actuator/health", - "/api/v3/api-docs/**", - "/api/swagger-ui/**", - "/api/swagger-resources/**", + "/actuator/health", + "/v3/api-docs/**", + "/swagger-ui/**", + "/swagger-resources/**", "/api/performances/detail/**", "/api/performances/booking/**", "/api/bookings/guest/**", "/api/schedules/**" -// "/login/oauth2/code/kakao", -// "/kakao/callback" }; @Bean From ef2d34ff97be3cd629ca19fd18672d3cea615f49 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 03:43:33 +0900 Subject: [PATCH 18/39] =?UTF-8?q?[feat]=20#54=20-=20=EB=93=B1=EB=A1=9D?= =?UTF-8?q?=ED=95=9C=20=EA=B3=B5=EC=97=B0=20=EB=AA=A9=EB=A1=9D=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20GET=20API=20=EA=B5=AC=ED=98=84=20(#55)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 --- .../booking/application/TicketService.java | 7 +--- .../api/PerformanceController.java | 9 ++++ .../application/PerformanceService.java | 31 ++++++++++++++ .../dto/MakerPerformanceDetail.java | 18 ++++++++ .../dto/MakerPerformanceResponse.java | 14 +++++++ .../dao/PerformanceRepository.java | 1 + .../exception/PerformanceSuccessCode.java | 3 +- src/main/resources/application-dev.yml | 41 ++++++++++--------- src/main/resources/application-prod.yml | 9 ++-- 9 files changed, 103 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java diff --git a/src/main/java/com/beat/domain/booking/application/TicketService.java b/src/main/java/com/beat/domain/booking/application/TicketService.java index 0918f02e..077506a9 100644 --- a/src/main/java/com/beat/domain/booking/application/TicketService.java +++ b/src/main/java/com/beat/domain/booking/application/TicketService.java @@ -8,6 +8,7 @@ import com.beat.domain.member.exception.MemberErrorCode; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.exception.PerformanceErrorCode; import com.beat.domain.schedule.domain.ScheduleNumber; import com.beat.domain.booking.exception.BookingErrorCode; import com.beat.global.common.exception.NotFoundException; @@ -38,7 +39,7 @@ public TicketRetrieveResponse getTickets(Long memberId, Long performanceId, Sche () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); Performance performance = performanceRepository.findById(performanceId) - .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); List bookings; @@ -52,10 +53,6 @@ public TicketRetrieveResponse getTickets(Long memberId, Long performanceId, Sche bookings = ticketRepository.findBySchedulePerformanceId(performanceId); } - if (bookings.isEmpty()) { - throw new NotFoundException(BookingErrorCode.NO_TICKETS_FOUND); - } - List bookingList = bookings.stream() .map(booking -> TicketDetail.of( booking.getId(), diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceController.java b/src/main/java/com/beat/domain/performance/api/PerformanceController.java index 9d501e57..1e54ae85 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceController.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -1,9 +1,11 @@ package com.beat.domain.performance.api; import com.beat.domain.performance.application.dto.BookingPerformanceDetailResponse; +import com.beat.domain.performance.application.dto.MakerPerformanceResponse; import com.beat.domain.performance.application.dto.PerformanceDetailResponse; import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.domain.performance.application.PerformanceService; +import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; @@ -35,4 +37,11 @@ public ResponseEntity> getBook BookingPerformanceDetailResponse bookingPerformanceDetail = performanceService.getBookingPerformanceDetail(performanceId); return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.BOOKING_PERFORMANCE_RETRIEVE_SUCCESS, bookingPerformanceDetail)); } + + @Operation(summary = "회원이 등록한 공연 목록 조회 API", description = "회원이 등록한 공연 목록을 조회하는 GET API입니다.") + @GetMapping("/user") + public ResponseEntity> getUserPerformances(@CurrentMember Long memberId) { + MakerPerformanceResponse response = performanceService.getMemberPerformances(memberId); + return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.MAKER_PERFORMANCE_RETRIEVE_SUCCESS, response)); + } } diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceService.java b/src/main/java/com/beat/domain/performance/application/PerformanceService.java index 59770817..62136665 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -1,4 +1,7 @@ package com.beat.domain.performance.application; +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.exception.MemberErrorCode; import com.beat.domain.performance.application.dto.*; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; @@ -10,6 +13,9 @@ import com.beat.domain.cast.dao.CastRepository; import com.beat.domain.schedule.domain.Schedule; import com.beat.domain.staff.dao.StaffRepository; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; import com.beat.global.common.exception.NotFoundException; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -27,6 +33,8 @@ public class PerformanceService { private final StaffRepository staffRepository; private final ScheduleService scheduleService; private final PromotionRepository promotionRepository; + private final MemberRepository memberRepository; + private final UserRepository userRepository; @Transactional(readOnly = true) public PerformanceDetailResponse getPerformanceDetail(Long performanceId) { @@ -168,4 +176,27 @@ private List getPromotions() { .collect(Collectors.toList()); } + @Transactional(readOnly = true) + public MakerPerformanceResponse getMemberPerformances(Long memberId) { + Member member = memberRepository.findById(memberId).orElseThrow( + () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = userRepository.findById(member.getUser().getId()).orElseThrow( + () -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + List performances = performanceRepository.findByUsersId(user.getId()); + + List performanceDetails = performances.stream() + .map(performance -> MakerPerformanceDetail.of( + performance.getId(), + performance.getGenre().name(), + performance.getPerformanceTitle(), + performance.getPosterImage(), + performance.getPerformancePeriod() + )) + .collect(Collectors.toList()); + + return MakerPerformanceResponse.of(user.getId(), performanceDetails); + } + } diff --git a/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java b/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java new file mode 100644 index 00000000..21dcaa61 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java @@ -0,0 +1,18 @@ +package com.beat.domain.performance.application.dto; + +public record MakerPerformanceDetail( + Long performanceId, + String genre, + String performanceTitle, + String posterImage, + String performancePeriod +) { + public static MakerPerformanceDetail of( + Long performanceId, + String genre, + String performanceTitle, + String posterImage, + String performancePeriod) { + return new MakerPerformanceDetail(performanceId, genre, performanceTitle, posterImage, performancePeriod); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java b/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java new file mode 100644 index 00000000..6a36683d --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java @@ -0,0 +1,14 @@ +package com.beat.domain.performance.application.dto; + +import java.util.List; + +public record MakerPerformanceResponse( + Long userId, + List performances +) { + public static MakerPerformanceResponse of( + Long userId, + List performances) { + return new MakerPerformanceResponse(userId, performances); + } +} diff --git a/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java index ac4d6a37..3df52002 100644 --- a/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java +++ b/src/main/java/com/beat/domain/performance/dao/PerformanceRepository.java @@ -8,5 +8,6 @@ public interface PerformanceRepository extends JpaRepository { List findByGenre(Genre genre); + List findByUsersId(Long userId); } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java index 2f228eb9..596b114f 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java @@ -9,7 +9,8 @@ public enum PerformanceSuccessCode implements BaseSuccessCode { PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."), BOOKING_PERFORMANCE_RETRIEVE_SUCCESS(200, "예매 관련 공연 정보 조회가 성공적으로 완료되었습니다."), - HOME_PERFORMANCE_RETRIEVE_SUCCESS(200, "홈 화면 공연 목록 조회가 성공적으로 완료되었습니다.") + HOME_PERFORMANCE_RETRIEVE_SUCCESS(200, "홈 화면 공연 목록 조회가 성공적으로 완료되었습니다."), + MAKER_PERFORMANCE_RETRIEVE_SUCCESS(200, "회원이 등록한 공연 목록의 조회가 성공적으로 완료되었습니다.") ; private final int status; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index b1daf617..cd963466 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -14,7 +14,7 @@ spring: jpa: hibernate: - ddl-auto: update + ddl-auto: create show-sql: true properties: hibernate: @@ -25,26 +25,27 @@ spring: redis: host: localhost port: 6379 - security: - oauth2: - client: - registration: - kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - redirect-uri: ${KAKAO_REDIRECT_URI} - client-authentication-method: POST - authorization-grant-type: authorization_code - scope: profile_nickname, account_email - client-name: Kakao - provider: - kakao: - authorization-uri: https://kauth.kakao.com/oauth/authorize - token-uri: https://kauth.kakao.com/oauth/token - user-info-uri: https://kapi.kakao.com/v2/user/me - user-name-attribute: id + + security: + oauth2: + client: + registration: + kakao: + client-id: ${DEV_KAKAO_CLIENT_ID} + client-secret: ${DEV_KAKAO_CLIENT_SECRET} + redirect-uri: ${DEV_KAKAO_REDIRECT_URI} + client-authentication-method: POST + authorization-grant-type: authorization_code + scope: profile_nickname, account_email + client-name: Kakao + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id jwt: - secret: ${JWT_SECRET} + secret: ${DEV_JWT_SECRET} access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 refresh-token-expire-time: 1209600000 # 14일 밀리초 diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e3d993a1..ee4fb9e4 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -24,14 +24,15 @@ spring: redis: host: localhost port: 6379 + security: oauth2: client: registration: kakao: - client-id: ${KAKAO_CLIENT_ID} - client-secret: ${KAKAO_CLIENT_SECRET} - redirect-uri: ${KAKAO_REDIRECT_URI} + client-id: ${PROD_KAKAO_CLIENT_ID} + client-secret: ${PROD_KAKAO_CLIENT_SECRET} + redirect-uri: ${PROD_KAKAO_REDIRECT_URI} client-authentication-method: POST authorization-grant-type: authorization_code scope: profile_nickname, account_email @@ -44,6 +45,6 @@ spring: user-name-attribute: id jwt: - secret: ${JWT_SECRET} + secret: ${PROD_JWT_SECRET} access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 refresh-token-expire-time: 1209600000 # 14일 밀리초 From 33e9d39ccde07d8576d15ee9261a6467ef757909 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 04:19:35 +0900 Subject: [PATCH 19/39] =?UTF-8?q?[fix]=20#58=20-=20dev-ci.yml,=20prod-ci.y?= =?UTF-8?q?ml=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=20=20(#59)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 --- .github/workflows/dev-CI.yml | 4 ++++ .github/workflows/prod-CI.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/.github/workflows/dev-CI.yml b/.github/workflows/dev-CI.yml index 111c2e83..d18bc861 100644 --- a/.github/workflows/dev-CI.yml +++ b/.github/workflows/dev-CI.yml @@ -43,6 +43,10 @@ jobs: DEV_DB_URL: ${{ secrets.DEV_DB_URL }} DEV_DB_USERNAME: ${{ secrets.DEV_DB_USERNAME }} DEV_DB_PASSWORD: ${{ secrets.DEV_DB_PASSWORD }} + DEV_JWT_SECRET: ${{ secrets.DEV_JWT_SECRET }} + DEV_KAKAO_CLIENT_ID: ${{ secrets.DEV_KAKAO_CLIENT_ID }} + DEV_KAKAO_CLIENT_SECRET: ${{ secrets.DEV_KAKAO_CLIENT_SECRET }} + DEV_KAKAO_REDIRECT_URI: ${{ secrets.DEV_KAKAO_REDIRECT_URI }} run: | cd ./src/main/resources envsubst < application-dev.yml > application-dev.tmp.yml && mv application-dev.tmp.yml application-dev.yml diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index f227a027..ccf55c6b 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -43,6 +43,10 @@ jobs: PROD_DB_URL: ${{ secrets.PROD_DB_URL }} PROD_DB_USERNAME: ${{ secrets.PROD_DB_USERNAME }} PROD_DB_PASSWORD: ${{ secrets.PROD_DB_PASSWORD }} + PROD_JWT_SECRET: ${{ secrets.PROD_JWT_SECRET }} + PROD_KAKAO_CLIENT_ID: ${{ secrets.PROD_KAKAO_CLIENT_ID }} + PROD_KAKAO_CLIENT_SECRET: ${{ secrets.PROD_KAKAO_CLIENT_SECRET }} + PROD_KAKAO_REDIRECT_URI: ${{ secrets.PROD_KAKAO_REDIRECT_URI }} run: | cd ./src/main/resources envsubst < application-prod.yml > application-prod.tmp.yml && mv application-prod.tmp.yml application-prod.yml From 3a49335f88e75d138bb5caf42e5e555c34a5ec24 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 05:10:49 +0900 Subject: [PATCH 20/39] =?UTF-8?q?[refactor]=20#61=20-=20Performance=20Enti?= =?UTF-8?q?ty=20field=20=EC=B6=94=EA=B0=80=20(#62)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 --- .../com/beat/domain/booking/api/BookingController.java | 2 +- .../com/beat/domain/performance/domain/Performance.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/beat/domain/booking/api/BookingController.java b/src/main/java/com/beat/domain/booking/api/BookingController.java index 975f18dc..9ec223e4 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -54,7 +54,7 @@ public ResponseEntity> getMemberB .body(SuccessResponse.of(BookingSuccessCode.MEMBER_BOOKING_SUCCESS, response)); } - @Operation(summary = "회원 예매 API", description = "비회원이 예매를 요청하는 POST API입니다.") + @Operation(summary = "비회원 예매 API", description = "비회원이 예매를 요청하는 POST API입니다.") @PostMapping("/guest") public ResponseEntity> createGuestBookings( @RequestBody GuestBookingRequest guestBookingRequest) { diff --git a/src/main/java/com/beat/domain/performance/domain/Performance.java b/src/main/java/com/beat/domain/performance/domain/Performance.java index 1226b990..ffec1b5b 100644 --- a/src/main/java/com/beat/domain/performance/domain/Performance.java +++ b/src/main/java/com/beat/domain/performance/domain/Performance.java @@ -42,6 +42,9 @@ public class Performance extends BaseTimeEntity { @Column(nullable = true) private String accountNumber; + @Column(nullable = true) + private String accountHolder; + @Column(nullable = false) private String posterImage; @@ -70,7 +73,7 @@ public class Performance extends BaseTimeEntity { @Builder public Performance(String performanceTitle, Genre genre, int runningTime, String performanceDescription, String performanceAttentionNote, - BankName bankName, String accountNumber, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, + BankName bankName, String accountNumber, String accountHolder, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, String performancePeriod, int ticketPrice, int totalScheduleCount, Users users) { this.performanceTitle = performanceTitle; this.genre = genre; @@ -79,6 +82,7 @@ public Performance(String performanceTitle, Genre genre, int runningTime, String this.performanceAttentionNote = performanceAttentionNote; this.bankName = bankName; this.accountNumber = accountNumber; + this.accountHolder = accountHolder; this.posterImage = posterImage; this.performanceTeamName = performanceTeamName; this.performanceVenue = performanceVenue; @@ -91,7 +95,7 @@ public Performance(String performanceTitle, Genre genre, int runningTime, String public static Performance create( String performanceTitle, Genre genre, int runningTime, String performanceDescription, String performanceAttentionNote, - BankName bankName, String accountNumber, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, + BankName bankName, String accountNumber, String accountHolder, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, String performancePeriod, int ticketPrice, int totalScheduleCount, Users users) { return Performance.builder() .performanceTitle(performanceTitle) @@ -101,6 +105,7 @@ public static Performance create( .performanceAttentionNote(performanceAttentionNote) .bankName(bankName) .accountNumber(accountNumber) + .accountHolder(accountHolder) .posterImage(posterImage) .performanceTeamName(performanceTeamName) .performanceVenue(performanceVenue) From 9728233e14504ed8fd35e9e493d6ade1778c755f Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 11:58:12 +0900 Subject: [PATCH 21/39] =?UTF-8?q?[feat]=20#63=20-=20healthCheckController?= =?UTF-8?q?=20=EC=83=9D=EC=84=B1=20(#64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 --- .../beat/domain/user/api/HealthCheckController.java | 10 ++++++++++ .../beat/domain/user/repository/UsersRepository.java | 7 ------- 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/beat/domain/user/api/HealthCheckController.java delete mode 100644 src/main/java/com/beat/domain/user/repository/UsersRepository.java diff --git a/src/main/java/com/beat/domain/user/api/HealthCheckController.java b/src/main/java/com/beat/domain/user/api/HealthCheckController.java new file mode 100644 index 00000000..6048ff6c --- /dev/null +++ b/src/main/java/com/beat/domain/user/api/HealthCheckController.java @@ -0,0 +1,10 @@ +package com.beat.domain.user.api; + +import org.springframework.web.bind.annotation.GetMapping; + +public class HealthCheckController { + @GetMapping("/health-check") + public String healthcheck() { + return "OK"; + } +} diff --git a/src/main/java/com/beat/domain/user/repository/UsersRepository.java b/src/main/java/com/beat/domain/user/repository/UsersRepository.java deleted file mode 100644 index cf274de7..00000000 --- a/src/main/java/com/beat/domain/user/repository/UsersRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.beat.domain.user.repository; - -import com.beat.domain.user.domain.Users; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UsersRepository extends JpaRepository { -} \ No newline at end of file From 8312edb4de8bf24c77109bca71af47249004b815 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 13:18:26 +0900 Subject: [PATCH 22/39] =?UTF-8?q?[feat]=20#63=20-=20health=20check=20URL?= =?UTF-8?q?=20=ED=99=94=EC=9D=B4=ED=8A=B8=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EB=93=B1=EB=A1=9D=20(#67)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 --- .../com/beat/domain/user/repository/UsersRepository.java | 7 ------- .../java/com/beat/global/common/config/SecurityConfig.java | 3 ++- 2 files changed, 2 insertions(+), 8 deletions(-) delete mode 100644 src/main/java/com/beat/domain/user/repository/UsersRepository.java diff --git a/src/main/java/com/beat/domain/user/repository/UsersRepository.java b/src/main/java/com/beat/domain/user/repository/UsersRepository.java deleted file mode 100644 index cf274de7..00000000 --- a/src/main/java/com/beat/domain/user/repository/UsersRepository.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.beat.domain.user.repository; - -import com.beat.domain.user.domain.Users; -import org.springframework.data.jpa.repository.JpaRepository; - -public interface UsersRepository extends JpaRepository { -} \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 4204ba13..4d9676a7 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -32,7 +32,8 @@ public class SecurityConfig { "/api/performances/detail/**", "/api/performances/booking/**", "/api/bookings/guest/**", - "/api/schedules/**" + "/api/schedules/**", + "/health-check" }; @Bean From f1997152fb17ad02d6b0a02362c064ef5d0d9a1c Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 16:34:40 +0900 Subject: [PATCH 23/39] =?UTF-8?q?[fix]=20#57=20-=20=20entity=20=EC=97=B0?= =?UTF-8?q?=EA=B4=80=EA=B4=80=EA=B3=84=20=EC=84=A4=EC=A0=95=20=20SecurityC?= =?UTF-8?q?onfig=20=EC=88=98=EC=A0=95=20(#65)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> --- .../com/beat/domain/cast/domain/Cast.java | 3 ++ .../com/beat/domain/member/domain/Member.java | 4 ++- .../performance/domain/Performance.java | 2 +- .../domain/promotion/domain/Promotion.java | 14 ++++++++ .../beat/domain/schedule/domain/Schedule.java | 3 ++ .../com/beat/domain/staff/domain/Staff.java | 33 +++++++++++++++---- .../global/common/config/SecurityConfig.java | 14 +++----- 7 files changed, 55 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/beat/domain/cast/domain/Cast.java b/src/main/java/com/beat/domain/cast/domain/Cast.java index 6577f5b4..00994322 100644 --- a/src/main/java/com/beat/domain/cast/domain/Cast.java +++ b/src/main/java/com/beat/domain/cast/domain/Cast.java @@ -13,6 +13,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -34,6 +36,7 @@ public class Cast{ @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "performance_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Performance performance; @Builder diff --git a/src/main/java/com/beat/domain/member/domain/Member.java b/src/main/java/com/beat/domain/member/domain/Member.java index 69a17f46..90951b30 100644 --- a/src/main/java/com/beat/domain/member/domain/Member.java +++ b/src/main/java/com/beat/domain/member/domain/Member.java @@ -6,6 +6,8 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; @@ -29,6 +31,7 @@ public class Member extends BaseTimeEntity { @ManyToOne @JoinColumn(name = "user_id", nullable = true) + @OnDelete(action = OnDeleteAction.CASCADE) private Users user; @Column(nullable = false) @@ -37,7 +40,6 @@ public class Member extends BaseTimeEntity { @Enumerated(EnumType.STRING) private SocialType socialType; - @Builder public Member(String nickname, String email, LocalDateTime deletedAt, Users user, Long socialId, SocialType socialType) { this.nickname = nickname; diff --git a/src/main/java/com/beat/domain/performance/domain/Performance.java b/src/main/java/com/beat/domain/performance/domain/Performance.java index ffec1b5b..be87dcc5 100644 --- a/src/main/java/com/beat/domain/performance/domain/Performance.java +++ b/src/main/java/com/beat/domain/performance/domain/Performance.java @@ -67,7 +67,7 @@ public class Performance extends BaseTimeEntity { private int totalScheduleCount; @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = true) // 테스트를 위한 false + @JoinColumn(name = "user_id", nullable = false) @OnDelete(action = OnDeleteAction.CASCADE) private Users users; diff --git a/src/main/java/com/beat/domain/promotion/domain/Promotion.java b/src/main/java/com/beat/domain/promotion/domain/Promotion.java index 3cd7521f..445469ef 100644 --- a/src/main/java/com/beat/domain/promotion/domain/Promotion.java +++ b/src/main/java/com/beat/domain/promotion/domain/Promotion.java @@ -3,6 +3,7 @@ import com.beat.domain.performance.domain.Performance; import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; @@ -21,4 +22,17 @@ public class Promotion { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "performance_id", nullable = false) private Performance performance; + + @Builder + public Promotion(String promotionPhoto, Performance performance) { + this.promotionPhoto = promotionPhoto; + this.performance = performance; + } + + public static Promotion create(String promotionPhoto, Performance performance) { + return Promotion.builder() + .promotionPhoto(promotionPhoto) + .performance(performance) + .build(); + } } diff --git a/src/main/java/com/beat/domain/schedule/domain/Schedule.java b/src/main/java/com/beat/domain/schedule/domain/Schedule.java index d78de579..cb31e43b 100644 --- a/src/main/java/com/beat/domain/schedule/domain/Schedule.java +++ b/src/main/java/com/beat/domain/schedule/domain/Schedule.java @@ -16,6 +16,8 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; import java.time.LocalDateTime; @@ -47,6 +49,7 @@ public class Schedule { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "performance_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) private Performance performance; @Builder diff --git a/src/main/java/com/beat/domain/staff/domain/Staff.java b/src/main/java/com/beat/domain/staff/domain/Staff.java index 4bc0101e..c7fb2515 100644 --- a/src/main/java/com/beat/domain/staff/domain/Staff.java +++ b/src/main/java/com/beat/domain/staff/domain/Staff.java @@ -1,13 +1,13 @@ package com.beat.domain.staff.domain; -import jakarta.persistence.Column; -import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; +import com.beat.domain.performance.domain.Performance; +import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; @Entity @Getter @@ -27,6 +27,25 @@ public class Staff { @Column(nullable = false) private String staffPhoto; - @Column(nullable = false) - private Long performanceId; + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = false) + @OnDelete(action = OnDeleteAction.CASCADE) + private Performance performance; + + @Builder + public Staff(String staffName, String staffRole, String staffPhoto, Performance performance) { + this.staffName = staffName; + this.staffRole = staffRole; + this.staffPhoto = staffPhoto; + this.performance = performance; + } + + public static Staff create(String staffName, String staffRole, String staffPhoto, Performance performance) { + return Staff.builder() + .staffName(staffName) + .staffRole(staffRole) + .staffPhoto(staffPhoto) + .performance(performance) + .build(); + } } diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 4d9676a7..a38a24a6 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -1,5 +1,6 @@ package com.beat.global.common.config; + import com.beat.global.auth.jwt.filter.JwtAuthenticationFilter; import com.beat.global.auth.security.CustomAccessDeniedHandler; import com.beat.global.auth.security.CustomJwtAuthenticationEntryPoint; @@ -17,23 +18,18 @@ @RequiredArgsConstructor @Configuration public class SecurityConfig { + private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; private static final String[] AUTH_WHITELIST = { - "/api/users/sign-up", - "/api/users/refresh-token", - "/api/users/sign-out", + "/api/**", + "/health-check", "/actuator/health", "/v3/api-docs/**", "/swagger-ui/**", - "/swagger-resources/**", - "/api/performances/detail/**", - "/api/performances/booking/**", - "/api/bookings/guest/**", - "/api/schedules/**", - "/health-check" + "/swagger-resources/**" }; @Bean From 3a92f78f83e3cd195dee20fc50e601a149c2ffa0 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 19:07:06 +0900 Subject: [PATCH 24/39] =?UTF-8?q?[fix]=20#57=20-=20security=20=EA=B4=80?= =?UTF-8?q?=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EB=8C=80=EC=9D=91=20(#71)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 --- .../global/auth/jwt/filter/JwtAuthenticationFilter.java | 7 +------ .../java/com/beat/global/common/config/SecurityConfig.java | 7 ++++++- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java index cb5a6e24..ae05332e 100644 --- a/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java @@ -39,13 +39,8 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } - } catch (Exception exception) { - try { - - throw new Exception(); - } catch (Exception e) { + } catch (Exception e) { log.info(e.getMessage()); - } } // 다음 필터로 요청 전달 filterChain.doFilter(request, response); diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 559b0141..f8f9fab2 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -9,6 +9,7 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @@ -30,7 +31,6 @@ public class SecurityConfig { "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**" - }; @Bean @@ -55,4 +55,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } + + @Bean + public WebSecurityCustomizer webSecurityCustomizer() { + return web -> web.ignoring().requestMatchers(AUTH_WHITELIST); + } } From 4a414f7645d233690e61d096d3a25d2382b5cad5 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Tue, 16 Jul 2024 21:07:29 +0900 Subject: [PATCH 25/39] =?UTF-8?q?[fix]=20#73=20-=20security=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#74)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 --- .../jwt/filter/JwtAuthenticationFilter.java | 7 +++---- .../auth/jwt/provider/JwtTokenProvider.java | 21 ++++++------------- .../global/common/config/SecurityConfig.java | 18 +++++++--------- 3 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java index ae05332e..051274bf 100644 --- a/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java +++ b/src/main/java/com/beat/global/auth/jwt/filter/JwtAuthenticationFilter.java @@ -32,20 +32,19 @@ protected void doFilterInternal(@NonNull HttpServletRequest request, @NonNull FilterChain filterChain) throws ServletException, IOException { try { final String token = getJwtFromRequest(request); - if (jwtTokenProvider.validateToken(token) == VALID_JWT) { + if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token) == VALID_JWT) { Long memberId = jwtTokenProvider.getUserFromJwt(token); - // authentication 객체 생성 -> principal에 유저정보를 담는다. + // authentication 객체 생성 -> principal에 유저 정보를 담는다. MemberAuthentication authentication = new MemberAuthentication(memberId.toString(), null, null); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } } catch (Exception e) { - log.info(e.getMessage()); + log.error("JwtAuthentication Authentication Exception Occurs! - {}", e.getClass(), e); } // 다음 필터로 요청 전달 filterChain.doFilter(request, response); } - private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java index f1c0908d..e0cba3ef 100644 --- a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -8,7 +8,6 @@ import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; -import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Date; import javax.crypto.SecretKey; @@ -27,10 +26,12 @@ public class JwtTokenProvider { private long REFRESH_TOKEN_EXPIRE_TIME; private static final String MEMBER_ID = "memberId"; + private SecretKey secretKey; @PostConstruct protected void init() { - JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); + byte[] keyBytes = Base64.getDecoder().decode(JWT_SECRET); + this.secretKey = Keys.hmacShaKeyFor(keyBytes); } public String issueAccessToken(final Authentication authentication) { @@ -40,11 +41,7 @@ public String issueAccessToken(final Authentication authentication) { public String issueRefreshToken(final Authentication authentication) { return issueToken(authentication, REFRESH_TOKEN_EXPIRE_TIME); } - - private String issueToken( - final Authentication authentication, - final Long expiredTime - ) { + private String issueToken(final Authentication authentication, final Long expiredTime) { final Date now = new Date(); final Claims claims = Jwts.claims() @@ -55,15 +52,9 @@ private String issueToken( return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) .setClaims(claims) - .signWith(getSigningKey()) + .signWith(secretKey) .compact(); } - - private SecretKey getSigningKey() { - String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); - return Keys.hmacShaKeyFor(encodedKey.getBytes()); - } - public JwtValidationType validateToken(String token) { try { final Claims claims = getBody(token); @@ -81,7 +72,7 @@ public JwtValidationType validateToken(String token) { private Claims getBody(final String token) { return Jwts.parserBuilder() - .setSigningKey(getSigningKey()) + .setSigningKey(secretKey) .build() .parseClaimsJws(token) .getBody(); diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index f8f9fab2..5fe43c24 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -1,6 +1,5 @@ package com.beat.global.common.config; - import com.beat.global.auth.jwt.filter.JwtAuthenticationFilter; import com.beat.global.auth.security.CustomAccessDeniedHandler; import com.beat.global.auth.security.CustomJwtAuthenticationEntryPoint; @@ -9,7 +8,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; -import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.web.SecurityFilterChain; @@ -23,9 +21,14 @@ public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; private final CustomJwtAuthenticationEntryPoint customJwtAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; - private static final String[] AUTH_WHITELIST = { - "/api/**", + "/api/users/sign-up", + "/api/users/refresh-token", + "/api/bookings/guest/**", + "/api/main", + "/api/performances/booking/**", + "/api/schedules/**", + "/api/notifications/**", "/health-check", "/actuator/health", "/v3/api-docs/**", @@ -55,9 +58,4 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti return http.build(); } - - @Bean - public WebSecurityCustomizer webSecurityCustomizer() { - return web -> web.ignoring().requestMatchers(AUTH_WHITELIST); - } -} +} \ No newline at end of file From 585cea3f5f3b89872afcf113aaf037218e298555 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 21:48:28 +0900 Subject: [PATCH 26/39] =?UTF-8?q?[#76]=20fix(SecurityConfig):=20Base64=20?= =?UTF-8?q?=EC=9D=B8=EC=BD=94=EB=94=A9=EC=9D=84=20=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#77)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/jwt/provider/JwtTokenProvider.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java index e0cba3ef..f53d76f7 100644 --- a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -8,6 +8,7 @@ import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Date; import javax.crypto.SecretKey; @@ -30,7 +31,8 @@ public class JwtTokenProvider { @PostConstruct protected void init() { - byte[] keyBytes = Base64.getDecoder().decode(JWT_SECRET); + // 평문 문자열을 Base64로 인코딩 + byte[] keyBytes = Base64.getEncoder().encode(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); this.secretKey = Keys.hmacShaKeyFor(keyBytes); } @@ -41,7 +43,8 @@ public String issueAccessToken(final Authentication authentication) { public String issueRefreshToken(final Authentication authentication) { return issueToken(authentication, REFRESH_TOKEN_EXPIRE_TIME); } - private String issueToken(final Authentication authentication, final Long expiredTime) { + + private String issueToken(final Authentication authentication, final long expiredTime) { final Date now = new Date(); final Claims claims = Jwts.claims() @@ -55,9 +58,10 @@ private String issueToken(final Authentication authentication, final Long expire .signWith(secretKey) .compact(); } + public JwtValidationType validateToken(String token) { try { - final Claims claims = getBody(token); + getBody(token); return JwtValidationType.VALID_JWT; } catch (MalformedJwtException ex) { return JwtValidationType.INVALID_JWT_TOKEN; @@ -82,4 +86,4 @@ public Long getUserFromJwt(String token) { Claims claims = getBody(token); return Long.valueOf(claims.get(MEMBER_ID).toString()); } -} +} \ No newline at end of file From 1b51be8829fc78529296feaeb6503449322d2c51 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 22:02:00 +0900 Subject: [PATCH 27/39] =?UTF-8?q?[fix]=20#76=20-=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EC=BD=94=EB=93=9C=20=EC=B6=94=EA=B0=80=20(#79)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 --- .../com/beat/global/auth/jwt/provider/JwtTokenProvider.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java index 614c7816..749bfe2e 100644 --- a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -8,6 +8,7 @@ import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; import jakarta.annotation.PostConstruct; +import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Date; import javax.crypto.SecretKey; @@ -59,7 +60,8 @@ private String issueToken(final Authentication authentication, final long expire public JwtValidationType validateToken(String token) { try { - getBody(token); + Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; } catch (MalformedJwtException ex) { return JwtValidationType.INVALID_JWT_TOKEN; From 6da3238d37cce7c25c5d5fbcea938c33968ef598 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Tue, 16 Jul 2024 23:16:25 +0900 Subject: [PATCH 28/39] =?UTF-8?q?[#81]=20fix(JwtTokenProvider):=20jwtsecre?= =?UTF-8?q?t=EC=9D=84=20=EC=9D=B8=EC=BD=94=EB=94=A9=ED=95=98=EA=B3=A0=20?= =?UTF-8?q?=EB=94=94=EC=BD=94=EB=94=A9=EC=9D=84=20=ED=95=98=EB=8F=84?= =?UTF-8?q?=EB=A1=9D=20=EC=88=98=EC=A0=95=20(#82)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/auth/jwt/provider/JwtTokenProvider.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java index 749bfe2e..1aac0d78 100644 --- a/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java +++ b/src/main/java/com/beat/global/auth/jwt/provider/JwtTokenProvider.java @@ -27,12 +27,10 @@ public class JwtTokenProvider { private long REFRESH_TOKEN_EXPIRE_TIME; private static final String MEMBER_ID = "memberId"; - private SecretKey secretKey; @PostConstruct protected void init() { - byte[] keyBytes = Base64.getEncoder().encode(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); - this.secretKey = Keys.hmacShaKeyFor(keyBytes); + JWT_SECRET = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes(StandardCharsets.UTF_8)); } public String issueAccessToken(final Authentication authentication) { @@ -54,10 +52,15 @@ private String issueToken(final Authentication authentication, final long expire return Jwts.builder() .setHeaderParam(Header.TYPE, Header.JWT_TYPE) .setClaims(claims) - .signWith(secretKey) + .signWith(getSigningKey()) .compact(); } + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(JWT_SECRET.getBytes()); //SecretKey 통해 서명 생성 + return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 + } + public JwtValidationType validateToken(String token) { try { Claims claims = getBody(token); @@ -76,7 +79,7 @@ public JwtValidationType validateToken(String token) { private Claims getBody(final String token) { return Jwts.parserBuilder() - .setSigningKey(secretKey) + .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); From e93fc8f656ff9e11eacbed86a3f6028543b937b7 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 00:44:33 +0900 Subject: [PATCH 29/39] =?UTF-8?q?[#85]=20chore(application.yml):=20?= =?UTF-8?q?=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95=20(#8?= =?UTF-8?q?6)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 4 ++-- src/main/resources/application-prod.yml | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index cd963466..95949472 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -14,7 +14,7 @@ spring: jpa: hibernate: - ddl-auto: create + ddl-auto: update show-sql: true properties: hibernate: @@ -23,7 +23,7 @@ spring: data: redis: - host: localhost + host: ${DEV_REDIS_HOST} port: 6379 security: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index ee4fb9e4..3b21c334 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -13,8 +13,8 @@ spring: password: ${PROD_DB_PASSWORD} jpa: hibernate: - ddl-auto: create - show-sql: false + ddl-auto: update + show-sql: true properties: hibernate: dialect: org.hibernate.dialect.MySQLDialect @@ -22,7 +22,7 @@ spring: data: redis: - host: localhost + host: ${PROD_REDIS_HOST} port: 6379 security: From e23659fa100c04cf1e00675b826f7089ac9d1895 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:09:17 +0900 Subject: [PATCH 30/39] =?UTF-8?q?[chore]=20#88=20-=20=EB=A0=88=EB=94=94?= =?UTF-8?q?=EC=8A=A4=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#89)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#88] chore(dev-CI.yml): dev서버 레디스 환경변수 설정 추가 * [#88] chore(prod-CI.yml): prod 서버 레디스 환경변수 설정 추가 --- .github/workflows/dev-CI.yml | 1 + .github/workflows/prod-CI.yml | 1 + 2 files changed, 2 insertions(+) diff --git a/.github/workflows/dev-CI.yml b/.github/workflows/dev-CI.yml index d18bc861..c16bdad4 100644 --- a/.github/workflows/dev-CI.yml +++ b/.github/workflows/dev-CI.yml @@ -47,6 +47,7 @@ jobs: DEV_KAKAO_CLIENT_ID: ${{ secrets.DEV_KAKAO_CLIENT_ID }} DEV_KAKAO_CLIENT_SECRET: ${{ secrets.DEV_KAKAO_CLIENT_SECRET }} DEV_KAKAO_REDIRECT_URI: ${{ secrets.DEV_KAKAO_REDIRECT_URI }} + DEV_REDIS_HOST: ${{ secrets.DEV_REDIS_HOST }} run: | cd ./src/main/resources envsubst < application-dev.yml > application-dev.tmp.yml && mv application-dev.tmp.yml application-dev.yml diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index ccf55c6b..1a9166c9 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -47,6 +47,7 @@ jobs: PROD_KAKAO_CLIENT_ID: ${{ secrets.PROD_KAKAO_CLIENT_ID }} PROD_KAKAO_CLIENT_SECRET: ${{ secrets.PROD_KAKAO_CLIENT_SECRET }} PROD_KAKAO_REDIRECT_URI: ${{ secrets.PROD_KAKAO_REDIRECT_URI }} + PROD_REDIS_HOST: ${{ secrets.PROD_REDIS_HOST }} run: | cd ./src/main/resources envsubst < application-prod.yml > application-prod.tmp.yml && mv application-prod.tmp.yml application-prod.yml From f519b0c82a2f01d4374eb045b9e90c6303381e50 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:13:44 +0900 Subject: [PATCH 31/39] [deploy] merge to main (#90) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deploy] merge to main (#83) * [deploy] merge to develop (#27) * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin * [feat] #28 - 비회원 예매 조회 POST API 구현 (#29) * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 * [feat] #30 - SwaggerConfig 및 WebConfig 구현 (#31) * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 * HOTFIX(workflows): push 이벤트 시 github action이 빌드 되지 않도록 수정 * HOTFIX(Jenkinsfile): 포트 수정 * [feat] #32 - 티켓 예매 가능 여부 GET API 구현 (#33) * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 * [feat] #17 - 카카오 소셜 로그인 API 구현 (#36) * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #34 - 비회원 예매 POST API 구현 (#35) * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 * [Refactor] #37 - 카카오 로그인 API response 수정 (#39) * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 * [feat] #40 - 소개 관련 공연 정보 조회 GET API 구현 (#41) * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename * [feat] #42 - 예매 관련 공연 정보 GET API 구현 (#43) * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 * [feat] #38 - 회원 예매 POST API 구현 (#45) * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 * [feat] #44 - 홈페이지 공연 및 홍보 조회 GET API 구현 (#47) * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 * [feat] #46 - 회원 예매 조회 GET API 구현 (#48) * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 * [feat] #49 - 예매자 관리 API 구현 (#51) * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 * HOTFIX(SecurityConfig): WHITELIST 경로명 수정 * [fix] #52 - auth whitelist 수정 (#53) * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #54 - 등록한 공연 목록 조회 GET API 구현 (#55) * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 * [fix] #58 - dev-ci.yml, prod-ci.yml 환경변수 추가 (#59) * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 * [refactor] #61 - Performance Entity field 추가 (#62) * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 * [feat] #63 - healthCheckController 생성 (#64) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [feat] #63 - health check URL 화이트리스트에 등록 (#67) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 * [fix] #57 - entity 연관관계 설정 SecurityConfig 수정 (#65) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [fix] #57 - security 관련 에러 대응 (#71) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [fix] #73 - security 수정 (#74) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 (#77) * [fix] #76 - 누락된 코드 추가 (#79) * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 * [#81] fix(JwtTokenProvider): jwtsecret을 인코딩하고 디코딩을 하도록 수정 (#82) --------- Co-authored-by: hyerinhwang-sailin * [deploy] merge to main (#87) * [deploy] merge to develop (#27) * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin * [feat] #28 - 비회원 예매 조회 POST API 구현 (#29) * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 * [feat] #30 - SwaggerConfig 및 WebConfig 구현 (#31) * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 * HOTFIX(workflows): push 이벤트 시 github action이 빌드 되지 않도록 수정 * HOTFIX(Jenkinsfile): 포트 수정 * [feat] #32 - 티켓 예매 가능 여부 GET API 구현 (#33) * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 * [feat] #17 - 카카오 소셜 로그인 API 구현 (#36) * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #34 - 비회원 예매 POST API 구현 (#35) * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 * [Refactor] #37 - 카카오 로그인 API response 수정 (#39) * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 * [feat] #40 - 소개 관련 공연 정보 조회 GET API 구현 (#41) * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename * [feat] #42 - 예매 관련 공연 정보 GET API 구현 (#43) * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 * [feat] #38 - 회원 예매 POST API 구현 (#45) * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 * [feat] #44 - 홈페이지 공연 및 홍보 조회 GET API 구현 (#47) * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 * [feat] #46 - 회원 예매 조회 GET API 구현 (#48) * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 * [feat] #49 - 예매자 관리 API 구현 (#51) * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 * HOTFIX(SecurityConfig): WHITELIST 경로명 수정 * [fix] #52 - auth whitelist 수정 (#53) * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #54 - 등록한 공연 목록 조회 GET API 구현 (#55) * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 * [fix] #58 - dev-ci.yml, prod-ci.yml 환경변수 추가 (#59) * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 * [refactor] #61 - Performance Entity field 추가 (#62) * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 * [feat] #63 - healthCheckController 생성 (#64) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [feat] #63 - health check URL 화이트리스트에 등록 (#67) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 * [fix] #57 - entity 연관관계 설정 SecurityConfig 수정 (#65) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [fix] #57 - security 관련 에러 대응 (#71) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [fix] #73 - security 수정 (#74) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 (#77) * [fix] #76 - 누락된 코드 추가 (#79) * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 * [#81] fix(JwtTokenProvider): jwtsecret을 인코딩하고 디코딩을 하도록 수정 (#82) * [#85] chore(application.yml): 환경변수 설정 (#86) --------- Co-authored-by: hyerinhwang-sailin --------- Co-authored-by: hyerinhwang-sailin From ade47e5448b6f47aae51778a9d4af6973b442781 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 01:26:27 +0900 Subject: [PATCH 32/39] Revert "[deploy] merge to main (#90)" (#91) This reverts commit f519b0c82a2f01d4374eb045b9e90c6303381e50. From 53ec2180872eba79721992e3b735ac4ce4423fc6 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 02:11:34 +0900 Subject: [PATCH 33/39] =?UTF-8?q?[fix]=20#93=20-=20=EB=88=84=EB=9D=BD?= =?UTF-8?q?=EB=90=9C=20=EC=96=B4=EB=85=B8=ED=85=8C=EC=9D=B4=EC=85=98=20?= =?UTF-8?q?=EB=B0=8F=20=EB=88=84=EB=9D=BD=EB=90=9C=20=EA=B2=BD=EB=A1=9C?= =?UTF-8?q?=EB=AA=85=20=EC=B6=94=EA=B0=80=20(#94)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#93] fix(HealthCheckController): 누락된 어노테이션 추가 * [#93] refactor(SecurityConfig): 누락된 경로명 추가 --- .../com/beat/domain/user/api/HealthCheckController.java | 7 ++++++- .../java/com/beat/global/common/config/SecurityConfig.java | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/beat/domain/user/api/HealthCheckController.java b/src/main/java/com/beat/domain/user/api/HealthCheckController.java index 6048ff6c..b8a9427a 100644 --- a/src/main/java/com/beat/domain/user/api/HealthCheckController.java +++ b/src/main/java/com/beat/domain/user/api/HealthCheckController.java @@ -1,9 +1,14 @@ package com.beat.domain.user.api; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/health-check") public class HealthCheckController { - @GetMapping("/health-check") + @GetMapping public String healthcheck() { return "OK"; } diff --git a/src/main/java/com/beat/global/common/config/SecurityConfig.java b/src/main/java/com/beat/global/common/config/SecurityConfig.java index 430b66b7..1d80eaf7 100644 --- a/src/main/java/com/beat/global/common/config/SecurityConfig.java +++ b/src/main/java/com/beat/global/common/config/SecurityConfig.java @@ -30,6 +30,7 @@ public class SecurityConfig { "/api/performances/booking/**", "/api/schedules/**", "/api/notifications/**", + "/api/performances/detail/**", "/health-check", "/actuator/health", "/v3/api-docs/**", From 56a98f3d75f848a6723d6d82097f72c58b9cc3f3 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Wed, 17 Jul 2024 15:40:47 +0900 Subject: [PATCH 34/39] =?UTF-8?q?[bug]=20#84=20-=20actuator/health=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EB=B0=8F=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#84] chore(build.gradle): 의존성 추가 * [#84] chore: health/actuator yml 설정 추가 * [#84] chore: redis yml 설정 추가 --- build.gradle | 1 + src/main/resources/application-dev.yml | 7 ++++++- src/main/resources/application-prod.yml | 5 +++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index eb9dbfbc..e6b9ff0d 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,7 @@ dependencies { // Spring implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' + implementation 'org.springframework.boot:spring-boot-starter-actuator' // Database runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 95949472..80c8945d 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -6,6 +6,11 @@ spring: activate: on-profile: dev +management: + endpoint: + health: + show-details: always + datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DEV_DB_URL} @@ -23,7 +28,7 @@ spring: data: redis: - host: ${DEV_REDIS_HOST} + host: redis port: 6379 security: diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 3b21c334..f76ad342 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -6,6 +6,11 @@ spring: activate: on-profile: prod +management: + endpoint: + health: + show-details: always + datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${PROD_DB_URL} From 965a53b65b67f88de56f1d045afd3a8aad026f0c Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Wed, 17 Jul 2024 16:05:02 +0900 Subject: [PATCH 35/39] =?UTF-8?q?[#84]=20chore:=20yml=20=EC=88=98=EC=A0=95?= =?UTF-8?q?=20(#99)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/resources/application-dev.yml | 12 ++++++------ src/main/resources/application-prod.yml | 13 +++++++------ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 80c8945d..0d1f9b46 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -1,22 +1,22 @@ server: port: 8080 -spring: - config: - activate: - on-profile: dev - management: endpoint: health: show-details: always +spring: + config: + activate: + on-profile: dev + datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${DEV_DB_URL} username: ${DEV_DB_USERNAME} password: ${DEV_DB_PASSWORD} - + jpa: hibernate: ddl-auto: update diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index f76ad342..767faaa6 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -1,21 +1,22 @@ server: port: 8080 -spring: - config: - activate: - on-profile: prod - management: endpoint: health: show-details: always +spring: + config: + activate: + on-profile: prod + datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: ${PROD_DB_URL} username: ${PROD_DB_USERNAME} password: ${PROD_DB_PASSWORD} + jpa: hibernate: ddl-auto: update @@ -27,7 +28,7 @@ management: data: redis: - host: ${PROD_REDIS_HOST} + host: redis port: 6379 security: From 350199ad3aeec3873eed87786ef50ddec42d1dc6 Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Wed, 17 Jul 2024 17:01:46 +0900 Subject: [PATCH 36/39] =?UTF-8?q?[bug]=20#101=20-=20yml,=20s3=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#102)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#101] chore: yml 수정 * [#101] feat: s3 추가 --- .github/workflows/dev-CI.yml | 2 + .github/workflows/prod-CI.yml | 2 + build.gradle | 6 +- .../com/beat/global/external/s3/S3Config.java | 38 +++++++++ .../external/s3/dao/FileRepository.java | 8 ++ .../beat/global/external/s3/domain/File.java | 21 +++++ .../external/s3/service/FileService.java | 77 +++++++++++++++++++ src/main/resources/application-dev.yml | 11 +++ src/main/resources/application-prod.yml | 11 +++ 9 files changed, 175 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/beat/global/external/s3/S3Config.java create mode 100644 src/main/java/com/beat/global/external/s3/dao/FileRepository.java create mode 100644 src/main/java/com/beat/global/external/s3/domain/File.java create mode 100644 src/main/java/com/beat/global/external/s3/service/FileService.java diff --git a/.github/workflows/dev-CI.yml b/.github/workflows/dev-CI.yml index c16bdad4..53366e1b 100644 --- a/.github/workflows/dev-CI.yml +++ b/.github/workflows/dev-CI.yml @@ -48,6 +48,8 @@ jobs: DEV_KAKAO_CLIENT_SECRET: ${{ secrets.DEV_KAKAO_CLIENT_SECRET }} DEV_KAKAO_REDIRECT_URI: ${{ secrets.DEV_KAKAO_REDIRECT_URI }} DEV_REDIS_HOST: ${{ secrets.DEV_REDIS_HOST }} + DEV_S3_ACCESS_KEY: ${{ secrets.DEV_S3_ACCESS_KEY }} + DEV_S3_SECRET_KEY: ${{ secrets.DEV_S3_SECRET_KEY }} run: | cd ./src/main/resources envsubst < application-dev.yml > application-dev.tmp.yml && mv application-dev.tmp.yml application-dev.yml diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index 1a9166c9..bdc82913 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -48,6 +48,8 @@ jobs: PROD_KAKAO_CLIENT_SECRET: ${{ secrets.PROD_KAKAO_CLIENT_SECRET }} PROD_KAKAO_REDIRECT_URI: ${{ secrets.PROD_KAKAO_REDIRECT_URI }} PROD_REDIS_HOST: ${{ secrets.PROD_REDIS_HOST }} + PROD_S3_ACCESS_KEY: ${{ secrets.PROD_S3_ACCESS_KEY }} + PROD_S3_SECRET_KEY: ${{ secrets.PROD_S3_SECRET_KEY }} run: | cd ./src/main/resources envsubst < application-prod.yml > application-prod.tmp.yml && mv application-prod.tmp.yml application-prod.yml diff --git a/build.gradle b/build.gradle index e6b9ff0d..3dd83f67 100644 --- a/build.gradle +++ b/build.gradle @@ -67,8 +67,12 @@ dependencies { // Oauth2 implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' - // Swagger + // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0' + + // aws + implementation 'io.awspring.cloud:spring-cloud-starter-aws:2.4.4' + implementation 'io.awspring.cloud:spring-cloud-starter-aws-secrets-manager-config:2.4.4' } jar { diff --git a/src/main/java/com/beat/global/external/s3/S3Config.java b/src/main/java/com/beat/global/external/s3/S3Config.java new file mode 100644 index 00000000..73eca54e --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/S3Config.java @@ -0,0 +1,38 @@ +package com.beat.global.external.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; + +@Configuration +public class S3Config { + + @Value("${cloud.aws.credentials.access-key}") + private String accessKey; + + @Value("${cloud.aws.credentials.secret-key}") + private String secretKey; + + @Value("${cloud.aws.region}") + private String region; + + @Bean + @Primary + public BasicAWSCredentials awsCredentialsProvider() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(accessKey, secretKey); + return basicAWSCredentials; + } + + @Bean + public AmazonS3 amazonS3() { + return AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(awsCredentialsProvider())) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/dao/FileRepository.java b/src/main/java/com/beat/global/external/s3/dao/FileRepository.java new file mode 100644 index 00000000..f1dfbd2a --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/dao/FileRepository.java @@ -0,0 +1,8 @@ +package com.beat.global.external.s3.dao; + + +import com.beat.global.external.s3.domain.File; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface FileRepository extends JpaRepository { +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/domain/File.java b/src/main/java/com/beat/global/external/s3/domain/File.java new file mode 100644 index 00000000..23e5ef1d --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/domain/File.java @@ -0,0 +1,21 @@ +package com.beat.global.external.s3.domain; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import lombok.Data; + +import java.util.Date; + +@Entity +@Data +public class File { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String fileName; + private String filePath; + private Date uploadTime; +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/service/FileService.java b/src/main/java/com/beat/global/external/s3/service/FileService.java new file mode 100644 index 00000000..4d6234c9 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/service/FileService.java @@ -0,0 +1,77 @@ +package com.beat.global.external.s3.service; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.beat.domain.performance.domain.Performance; +import com.beat.global.external.s3.dao.FileRepository; +import com.beat.global.external.s3.domain.File; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; + +import java.net.URL; +import java.util.Date; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class FileService { + + @Value("${cloud.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + private final FileRepository fileRepository; + + public Map getPresignedUrl(String prefix, String fileName) { + String filePath = fileName; + if (!prefix.isEmpty()) { + filePath = createPath(prefix, fileName); + } + + GeneratePresignedUrlRequest generatePresignedUrlRequest = getGeneratePresignedUrlRequest(bucket, filePath); + URL url = amazonS3.generatePresignedUrl(generatePresignedUrlRequest); + + // 파일 정보를 데이터베이스에 저장 + saveFileToDB(fileName, filePath); + + return Map.of("url", url.toString()); + } + + private GeneratePresignedUrlRequest getGeneratePresignedUrlRequest(String bucket, String fileName) { + return new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(getPresignedUrlExpiration()); + } + + private Date getPresignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 2; + expiration.setTime(expTimeMillis); + + return expiration; + } + + private String createFileId() { + return UUID.randomUUID().toString(); + } + + private String createPath(String prefix, String fileName) { + String fileId = createFileId(); + return String.format("%s/%s", prefix, fileId + "-" + fileName); + } + + private void saveFileToDB(String fileName, String filePath) { +// Performance performance = new Performance(); +// performance.setPosterImage(filePath); + File file = new File(); + file.setFileName(fileName); + file.setFilePath(filePath); + file.setUploadTime(new Date()); + + fileRepository.save(file); + } +} \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 0d1f9b46..a4c92fe9 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -54,3 +54,14 @@ jwt: secret: ${DEV_JWT_SECRET} access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 refresh-token-expire-time: 1209600000 # 14일 밀리초 + +cloud: + aws: + region: ap-northeast-2 + credentials: + access-key: ${DEV_S3_ACCESS_KEY} + secret-key: ${DEV_S3_SECRET_KEY} + s3: + bucket: beat-dev-bucket + stack: + auto: false \ No newline at end of file diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 767faaa6..4088da83 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -54,3 +54,14 @@ jwt: secret: ${PROD_JWT_SECRET} access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 refresh-token-expire-time: 1209600000 # 14일 밀리초 + +cloud: + aws: + region: ap-northeast-2 + credentials: + access-key: ${PROD_S3_ACCESS_KEY} + secret-key: ${PROD_S3_SECRET_KEY} + s3: + bucket: beat-prod-bucket + stack: + auto: false \ No newline at end of file From 4a1c7b26f4956ef12b735a2508cedde60b5498df Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 17:12:33 +0900 Subject: [PATCH 37/39] =?UTF-8?q?HOTIFX:=20prod=20ci-yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/prod-CI.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index bdc82913..50d0cde3 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -3,6 +3,8 @@ name: prod-CI on: pull_request: branches: [ "main" ] + push: + branches: [ "main" ] jobs: prod-ci: From 66585cb9f16e5041fd8c9f38d5f5f5ac58e1fcbd Mon Sep 17 00:00:00 2001 From: hyerinhwang-sailin Date: Wed, 17 Jul 2024 17:49:59 +0900 Subject: [PATCH 38/39] =?UTF-8?q?Main=20redis=20host=20yml=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#105)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [deploy] merge to main (#104) * [deploy] merge to develop (#27) * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin * [feat] #28 - 비회원 예매 조회 POST API 구현 (#29) * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 * [feat] #30 - SwaggerConfig 및 WebConfig 구현 (#31) * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 * HOTFIX(workflows): push 이벤트 시 github action이 빌드 되지 않도록 수정 * HOTFIX(Jenkinsfile): 포트 수정 * [feat] #32 - 티켓 예매 가능 여부 GET API 구현 (#33) * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 * [feat] #17 - 카카오 소셜 로그인 API 구현 (#36) * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #34 - 비회원 예매 POST API 구현 (#35) * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 * [Refactor] #37 - 카카오 로그인 API response 수정 (#39) * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 * [feat] #40 - 소개 관련 공연 정보 조회 GET API 구현 (#41) * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename * [feat] #42 - 예매 관련 공연 정보 GET API 구현 (#43) * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 * [feat] #38 - 회원 예매 POST API 구현 (#45) * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 * [feat] #44 - 홈페이지 공연 및 홍보 조회 GET API 구현 (#47) * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 * [feat] #46 - 회원 예매 조회 GET API 구현 (#48) * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 * [feat] #49 - 예매자 관리 API 구현 (#51) * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 * HOTFIX(SecurityConfig): WHITELIST 경로명 수정 * [fix] #52 - auth whitelist 수정 (#53) * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #54 - 등록한 공연 목록 조회 GET API 구현 (#55) * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 * [fix] #58 - dev-ci.yml, prod-ci.yml 환경변수 추가 (#59) * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 * [refactor] #61 - Performance Entity field 추가 (#62) * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 * [feat] #63 - healthCheckController 생성 (#64) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [feat] #63 - health check URL 화이트리스트에 등록 (#67) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 * [fix] #57 - entity 연관관계 설정 SecurityConfig 수정 (#65) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [fix] #57 - security 관련 에러 대응 (#71) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [fix] #73 - security 수정 (#74) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 (#77) * [fix] #76 - 누락된 코드 추가 (#79) * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 * [#81] fix(JwtTokenProvider): jwtsecret을 인코딩하고 디코딩을 하도록 수정 (#82) * [#85] chore(application.yml): 환경변수 설정 (#86) * [chore] #88 - 레디스 환경변수 추가 (#89) * [#88] chore(dev-CI.yml): dev서버 레디스 환경변수 설정 추가 * [#88] chore(prod-CI.yml): prod 서버 레디스 환경변수 설정 추가 * [deploy] merge to main (#90) * [deploy] merge to main (#83) * [deploy] merge to develop (#27) * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin * [feat] #28 - 비회원 예매 조회 POST API 구현 (#29) * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 * [feat] #30 - SwaggerConfig 및 WebConfig 구현 (#31) * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 * HOTFIX(workflows): push 이벤트 시 github action이 빌드 되지 않도록 수정 * HOTFIX(Jenkinsfile): 포트 수정 * [feat] #32 - 티켓 예매 가능 여부 GET API 구현 (#33) * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 * [feat] #17 - 카카오 소셜 로그인 API 구현 (#36) * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #34 - 비회원 예매 POST API 구현 (#35) * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 * [Refactor] #37 - 카카오 로그인 API response 수정 (#39) * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 * [feat] #40 - 소개 관련 공연 정보 조회 GET API 구현 (#41) * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename * [feat] #42 - 예매 관련 공연 정보 GET API 구현 (#43) * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 * [feat] #38 - 회원 예매 POST API 구현 (#45) * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 * [feat] #44 - 홈페이지 공연 및 홍보 조회 GET API 구현 (#47) * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 * [feat] #46 - 회원 예매 조회 GET API 구현 (#48) * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 * [feat] #49 - 예매자 관리 API 구현 (#51) * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 * HOTFIX(SecurityConfig): WHITELIST 경로명 수정 * [fix] #52 - auth whitelist 수정 (#53) * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #54 - 등록한 공연 목록 조회 GET API 구현 (#55) * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 * [fix] #58 - dev-ci.yml, prod-ci.yml 환경변수 추가 (#59) * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 * [refactor] #61 - Performance Entity field 추가 (#62) * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 * [feat] #63 - healthCheckController 생성 (#64) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [feat] #63 - health check URL 화이트리스트에 등록 (#67) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 * [fix] #57 - entity 연관관계 설정 SecurityConfig 수정 (#65) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [fix] #57 - security 관련 에러 대응 (#71) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [fix] #73 - security 수정 (#74) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 (#77) * [fix] #76 - 누락된 코드 추가 (#79) * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 * [#81] fix(JwtTokenProvider): jwtsecret을 인코딩하고 디코딩을 하도록 수정 (#82) --------- Co-authored-by: hyerinhwang-sailin * [deploy] merge to main (#87) * [deploy] merge to develop (#27) * [deploy] merge to main (#26) * [#2] chore(.gitignore): .gradle, .idea 파일 삭제 (#3) * [refactor] #4 - 도메인형 디렉터리 구조 변경 및 local, dev, prod 운영 환경 분리 (#5) * [#4] fix: 클래스 첫글자 대문자로 수정 * [#4] remove: enum 삭제 * [#4] feat(BaseSuccessCode): 성공 상태 관리를 위한 인터페이스 생성 * [#4] feat(BaseErrorCode): 에러 상태 관리를 위한 인터페이스 생성 * [#4] refactor: global 패키지로 이동 * [#4] refactor: 서버 운영 환경 분리 * [feat] #6 - dev, prod Dockerfile 분리 및 github Action CI workflow 구현 (#7) * [#6] feat(Dockerfile): prod용 도커 파일 구현 * [#6] feat(Dockerfile-dev): dev용 도커 파일 구현 * [#6] feat(dev-CI.yml): dev용 CI workflow 구현 * [#6] feat(prod-CI.yml): prod용 CI workflow 구현 * [#6] chore(.gitignore): gradle-wrapper.jar 파일은 레포지토리에 포함되도록 설정 * [#6] fix(gradle-wrapper.jar): .gitignore로 누락된 파일 추가 * [#6] chore(build.grade): plain jar 생성 방지 * [#9] docs(README.md): 서비스 소개 README v1 작성 (#10) * [#11] docs(README.md): 리드미 업데이트 (#12) * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [feat] #14 - 회원, 홍보, 등장인물, 스태프 엔티티 생성 (#16) * [#14] feat(Member): 멤버 엔티티 생성 * [#14] feat(Promotion): 홍보 엔티티 생성 * [#14] feat(Cast): 등장인물 엔티티 생성 * [#14] feat(Staff): 스태프 엔티티 생성 * [feat] #13 - 유저, 예매, 공연, 회차 엔티티 생성 (#15) * [#13] feat(Users): 사용자 엔티티 생성 * [#13] feat(Genre): 장르 열거형 생성 * [#13] feat(BankName): 은행명 열거형 생성 * [#13] refactor(BaseTimeEntity): 업데이트 시간 관리 필드 활성화 * [#13] feat(Performance): 공연 엔티티 생성 * [#13] feat(ScheduleNumber): 회차 번호 열거형 생성 * [#13] feat(Schedule): 회차 엔티티 생성 * [#13] feat(Booking): 예매 엔티티 생성 * [#13] refactor(ScheduleNumber): 네이밍 수정 * [#14} fix: 스태프, 등장인물, 홍보 엔티티 상속관계 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [#18] feat(Jenkinsfile): 젠킨스 파일 생성 (#19) * [#20] refactor(Jenkinsfile): 젠킨스 파일 Webhook 테스트용 커밋 (#21) * [#22] feat(Jenkinsfile): Jenkins multibranch 스크립트 작성 (#23) * HOTFIX(Jenkinsfile): Jenkins multibranch 스크립트 수정 * HOTFIX(workflows): 빌드 후 젠킨스 배포가 진행되도록 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * HOTFIX(workflows): Jenkinsfile 스크립트 수정 * [#24] feat(Jenkinsfile): slack 연동 스크립트 작성 (#25) --------- Co-authored-by: hyerinhwang-sailin * HOTFIX(workflow): 오타 수정 --------- Co-authored-by: hyerinhwang-sailin * [feat] #28 - 비회원 예매 조회 POST API 구현 (#29) * chore(application-dev.yml): dialect 추가 * chore(application-prod.yml): dialect 추가 * [#28] chore(build.gradle): security 의존성 비활성화 - security 일시적으로 비활성화 * [#28] refactor(Booking): 예매 엔티티에 빌더 및 정적팩토리 메서드 추가 * [#28] refactor(ErrorResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(Schedule): 회차 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(Performance): 공연 엔티티에 빌더 및 정적 팩토리 메서드 추가 * [#28] refactor(SuccessResponse): 정적 팩토리 메서드 네이밍 변환 * [#28] refactor(GlobalExceptionHandler): INTERNAL_SERVER_ERROR 핸들러 메서드 추가 * [#28] feat(BookingSuccessCode): 예매 성공 메시지를 관리하는 열거형 생성 * [#28] feat(BookingErrorCode): 예매 에러 메시지를 관리하는 열거형 생성 * [#28] feat(BookingRetrieveResponse): 예매 조회 응답 DTO 생성 * [#28] feat(BookingRetrieveRequest): 예매 조회 요청 DTO 생성 * [#28] feat(BookingRepository): 예매 레포지토리 생성 및 비회원 예매 조회 시 예매 내역 가져오는 메서드 구현 * [#28] feat(BookingService): 예매 서비스 레이어 생성 및 비회원 조회 메서드 구현 * [#28] feat(BookingController): 예매 컨트롤러 생성 및 비회원 예매조회 POST API 구현 * [feat] #30 - SwaggerConfig 및 WebConfig 구현 (#31) * [#30] chore(build.gradle): springdoc 의존성 추가 * [#30] feat(SwaggerConfig): SwaggerConfig 추가 * [#30] feat(WebConfig): WebConfig 추가 * HOTFIX(workflows): push 이벤트 시 github action이 빌드 되지 않도록 수정 * HOTFIX(Jenkinsfile): 포트 수정 * [feat] #32 - 티켓 예매 가능 여부 GET API 구현 (#33) * [#32] refactor(BookingController): 메서드명 수정 * [#32] feat(ConflictException): Conflict 409에러 클래스 생성 * [#32] feat(GlobalExceptionHandler): ConflictException 등록 * [#32] feat(TicketAvailabilityRequest): TicketAvailabilityRequest DTO 생성 * [#32] feat(TicketAvailabilityResponse): TicketAvailabilityResponse DTO 생성 * [#32] feat(ScheduleSuccessCode): ScheduleSuccessCode 열거형 생성 * [#32] feat(ScheduleErrorCode): ScheduleErrorCode 열거형 생성 * [#32] feat(ScheduleRepository): ScheduleRepository 생성 * [#32] feat(ScheduleService): ScheduleService 생성 및 회차별 티켓 구매 가능 여부 판단 메서드 구현 * [#32] feat(ScheduleController): 회차별 티켓 수량 조회 GET API 구현 * [feat] #17 - 카카오 소셜 로그인 API 구현 (#36) * [#17] feat(build.gradle): jwt, security, open feign, Redis 의존성 추가 * [#17] feat(BeatApplication): OpenFeign 관련 어노테이션 추가 * [#17] feat(RedisConfig): redis 설정 * [#17] feat: redis 활용 jwt 토큰 생성 로직 구현 * [#17] feat(SecurityConfig): security 설정 * [#17] refactor: Member, Users 엔티티 수정 및 관련 enum 추가 * [#17] feat: Member, Users 도메인 관련 인증 로직 구현 * [#17] feat(MemberController): Member 관련 API 엔드포인트 구현 * [#17] feat: Security 관련 인증 객체 구현 * [#17] feat: 소셜로그인 로직 구현 * [#17] fix: 소셜로그인 오류 해결 * [#17] refactor: 코드리뷰 반영 * [#17] refactor: 코드리뷰 반영 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #34 - 비회원 예매 POST API 구현 (#35) * [#34] feat(Users): 빌더 및 정적 팩토리 메서드 추가 * [#34] feat(Entity): 엔티티 id 필드 수정 * [#34] refactor(application.yml): dialect 추가 * [#34] refactor(ScheduleService): 네이밍 수정 * [#34] refactor(BookingSuccessCode): 비회원 예매 조회 성공 메시지 추가 * [#34] feat(GuestBookingRequest): 비회원 예매 요청 DTO 생성 * [#34] feat(GuestBookingResponse): 비회원 예매 응답 DTO 생성 * [#34] refactor(BookingRetrieveService): 네이밍 수정 * [#34] feat(PerformanceRepository): 공연 레포지토리 생성 * [#34] feat(UserRepository): 유저 레포지토리 생성 * [#34] refactor(BookingRepository): 비회원 예매 시 네가지 정보가 모두 일치 해야만 다른 유저로 판단하는 메서드 구현 * [#34] refactor(ScheduleRepository): 비관적 락 적용 * [#34] feat(GuestBookingService): 비관적 락을 이용해 비회원 예매 로직 구현 * [#34] feat(BookingController): 비회원 예매 POST API 구현 * [#34] test(GuestBookingServiceConcurrencyTest): 동시성 테스트 구현 * [Refactor] #37 - 카카오 로그인 API response 수정 (#39) * [#37] rename: 빈 충돌 해결 위해 UserRepository를 UsersRepository로 rename * [#37] refactor: login response에서 refreshToken을 cookie에 담아주도록 변경 * [#37] fix: dao, repository 충돌 해결 * [#37] refactor: 어노테이션 수정 * [feat] #40 - 소개 관련 공연 정보 조회 GET API 구현 (#41) * [#40] feat: repository 추가 * [#40] feat: 공연 관련 성공, 실패 메세지 추가 * [#40] feat(PerformanceDetailResponse): 공연 상세페이지 조회 응답 dto * [#40] feat(PerformanceService): 공연 상세페이지 조회 로직 구현 * [#40] feat(PerformanceController): 공연 상세페이지 엔드포인트 구현 * [#40] feat(SecurityConfig): auth whitelist update * [#40] refactor: dto 분리 및 rename * [feat] #42 - 예매 관련 공연 정보 GET API 구현 (#43) * [#42] feat: 예매 관련 정보 조회 dto 생성 * [#42] feat: 예매 관련 정보 조회 성공 메세지 추가 * [#42] feat(ScheduleService): 잔여티켓계산, 예매가능여부확인, 예매가능여부 update 메소드 구현 * [#42] feat(PerformanceService): 예매관련공연정보의 response 생성하는 로직 구현 * [#42] feat(PerformanceController): 예매관련공연정보 조회 엔드포인트 구현 * [#42] feat(SecurityConfig): auth_whitelist update * [#42] refactor(ScheduleService): findTicketAvailability에서 getAvailableTicketCount method 사용하도록 수정 * [feat] #38 - 회원 예매 POST API 구현 (#45) * [#38] fix(Cast): Join 하는 부분 수정 및 빌더와 정적 팩토리 메서드 패턴 도입 * [#38] chore(application.yml): 계층 수정 * [#38] feat: 커스텀 어노테이션 생성 * [#38] fix(UserRepository): 중복된 클래스 삭제 * [#38] feat(UserErrorCode): 유저의 에러 메시지를 관리하는 열거형 생성 * [#38] refactor(MemberErrorCode): message 목적에 맞게 변경 * [#38] refactor(BookingSuccessCode): 회원 예매 성공 메시지 추가 * [#38] feat: 커스텀 어노테이션 생성 커밋에 누락된 클래스 추가 * [#38] refactor(GuestBookingService): schedule에서 바로 get 하도록 수정 * [#38] test(GuestBookingServiceConcurrencyTest): import문 경로명 변경 * [#38] refactor(MemberService): @Transactional 어노테이션이 필요한 메서드에 해당 옵션 추가 * [#38] feat(MemberBookingRequest): 회원 예매 요청 DTO 생성 * [#38] feat(MemberBookingResponse): 회원 예매 응답 DTO 생성 * [#38] refactor(MemberController): refreshToken을 쿠키에 넣어주도록 구현 * [#38] feat(MemberBookingService): 회원 예매 서비스 로직 구현 * [#38] feat(BookingController): 회원 예매 요청 POST API 구현 * [#38] fix: 빌드 안되는 부분 수정 * [#38] remove(MemberBookingService): 사용하지 않는 import문 삭제 * [#38] refactor: 커스텀 어노테이션 이름 수정 * [feat] #44 - 홈페이지 공연 및 홍보 조회 GET API 구현 (#47) * [#44] refactor: 코드리뷰 반영 * [#44] fix(Promotion): 기본키 이름 수정, performanceId 연관관계 설정 * [#44] feat: Repository 및 관련 로직 추가 * [#44] feat: errorcode, successcode 추가 * [#44] feat: 홈페이지 request, response dto 생성 * [#44] feat(ScheduleService): dueDate 관련 로직 구현 * [#44] feat(PerformanceService): 홈페이지 정렬 및 response 생성 로직 구현 * [#44] chore: import문 추가 * [feat] #46 - 회원 예매 조회 GET API 구현 (#48) * [#46] remove(BookingRetrieveRequest): 클래스 삭제 * [#46] refactor(BookingSuccessCode): 회원 예매 조회 성공 메시지 추가 * [#46] refactor(GuestBookingRetrieveResponse): 클래스 명 변경 * [#46] refactor(GuestBookingRetrieveRequest): 안쓰는 코드 주석처리 * [#46] refactor(GuestBookingRetrieveService): 클래스 명 수정으로 인한 리팩토링 * [#46] refactor(SecurityConfig): 토큰 사용하지 않는 api 경로 추가하기 * [#46] refactor(BookingRepository): userId로 예약 정보를 가져오는 JPA 메서드 구현 * [#46] feat(MemberBookingRetrieveResponse): 회원 조회 응답 DTO 생성 * [#46] feat(MemberBookingRetrieveService): 회원 예매 조회 로직을 담당하는 서비스 레이어 구현 * [#46] feat(BookingController): 회원 예매 조회 GET API 구현 * [feat] #49 - 예매자 관리 API 구현 (#51) * [#49] feat: 티켓 관련 success, error code 추가 * [#49] feat(Booking): 입금여부 setter 메소드 추가 * [#49] feat(TicketRetrieveResponse): 예매자 확인 response dto 생성 * [#49] feat(Ticket): 예매자 확인용 티켓 정보 response dto 생성 * [#49] feat(TicketRepository): 예매자 필터링용 메소드 생성 * [#49] feat: 예매자 입금정보 수정 request dto 생성 * [#49] feat(TicketDeleteRequest): 예매자 삭제 request dto 생성 * [#49] feat(TicketService): 예매자 관리 관련 로직 구현 * [#49] feat(TicketController): 예매자 관리 엔드포인트 구현 * [#49] chore: swagger annotation 추가 * HOTFIX(SecurityConfig): WHITELIST 경로명 수정 * [fix] #52 - auth whitelist 수정 (#53) * [#52]fix(SecurityConfig): auth_whitelist 수정 * [#52]fix(SecurityConfig): auth_whitelist 수정 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [feat] #54 - 등록한 공연 목록 조회 GET API 구현 (#55) * [#54] feat(PerformanceSuccessCode): success code 추가 * [#54] feat: 회원이 등록한 공연목록 조회 dto 생성 * [#54] feat(PerformanceService): 회원이 등록한 공연목록 조회 로직 구현 * [#54] feat(PerformanceRepository): 회원이 등록한 공연목록 조회 관련 메소드 추가 * [#54] feat(PerformanceController): 회원이 등록한 공연목록 조회 엔드포인트 생성 * [#54] fix(TicketService): exception error 수정 * [#54] chore(application.yml): yml 수정 * [fix] #58 - dev-ci.yml, prod-ci.yml 환경변수 추가 (#59) * [#58] fix(dev-CI.yml): Dev 서버 CI 워크플로우 수정 * [#58] fix(prod-CI.yml): Prod 서버 CI 워크플로우 수정 * [refactor] #61 - Performance Entity field 추가 (#62) * [#61] refactor(Performance): bankHolder 필드 추가 * [#61] chore(BookingController): swagger annotation 수정 * [feat] #63 - healthCheckController 생성 (#64) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [feat] #63 - health check URL 화이트리스트에 등록 (#67) * [#63] remove(UsersRepository): 중복 클래스 삭제 * [#63] feat(HealthCheckController): healthcheck 컨트롤러 생성 * [#63] feat(SecurityConfig): healthcheck url white_list에 등록 * [fix] #57 - entity 연관관계 설정 SecurityConfig 수정 (#65) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> * [fix] #57 - security 관련 에러 대응 (#71) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [fix] #73 - security 수정 (#74) * [#57] refactor: entity 연관관계 설정 및 수정 * [#57] fix(SecurityConfig): auth_whitelist update * [#57] chore(UsersRepository): 중복된 파일 및 불필요한 패키지 제거 * [#57] refactor(SecurityConfig): auth_whitelist update * [#57] refactor: 엔티티 정적팩토리 메소드 추가 * [#57] comment(SecurityConfig): 주석 제거 * [#57] fix(JwtAuthenticationFilter): 에러 로직 수정 * fix(SecurityConfig): webSecurityCustomizer 로직 추가 * [#57] fix:security 수정 * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 (#77) * [fix] #76 - 누락된 코드 추가 (#79) * [#76] fix(SecurityConfig): Base64 인코딩을 하도록 변경 * [#76] chore(SecurityConfig): 누락된 import문 추가 * [#76] chore(JwtTokenProvider): 누락된 코드 추가 * [#81] fix(JwtTokenProvider): jwtsecret을 인코딩하고 디코딩을 하도록 수정 (#82) * [#85] chore(application.yml): 환경변수 설정 (#86) --------- Co-authored-by: hyerinhwang-sailin --------- Co-authored-by: hyerinhwang-sailin * Revert "[deploy] merge to main (#90)" (#91) This reverts commit f519b0c82a2f01d4374eb045b9e90c6303381e50. * [fix] #93 - 누락된 어노테이션 및 누락된 경로명 추가 (#94) * [#93] fix(HealthCheckController): 누락된 어노테이션 추가 * [#93] refactor(SecurityConfig): 누락된 경로명 추가 * [bug] #84 - actuator/health 의존성 및 설정 추가 (#96) * [#84] chore(build.gradle): 의존성 추가 * [#84] chore: health/actuator yml 설정 추가 * [#84] chore: redis yml 설정 추가 * [#84] chore: yml 수정 (#99) * [bug] #101 - yml, s3 수정 (#102) * [#101] chore: yml 수정 * [#101] feat: s3 추가 * HOTIFX: prod ci-yml 수정 --------- Co-authored-by: hyerinhwang-sailin * Update application-prod.yml --------- Co-authored-by: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> --- src/main/resources/application-prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index 64549a5d..683046ee 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -28,7 +28,7 @@ spring: data: redis: - host: redis + host: beat-prod-redis port: 6379 security: From af6a92383078e7ca48e23ba7721a30c1a729b128 Mon Sep 17 00:00:00 2001 From: DongHoon Lee <125895298+hoonyworld@users.noreply.github.com> Date: Wed, 17 Jul 2024 20:55:55 +0900 Subject: [PATCH 39/39] =?UTF-8?q?[feat]=20#50=20-=20=EA=B3=B5=EC=97=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20POST=20API=20=EC=83=9D=EC=84=B1=20(#108)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [#50] chore: 폴더 이동 * [#50] feat: request DTO 생성 * [#50] feat: response DTO 생성 * [#50] refactor(PerformanceSuccessCode): 공연 생성 성공 메시지 추가 * [#50] chore(PerformanceService): import문 변 * [#50] chore(HomeController): import문 변 * [#50] feat(PerformanceCreateService): 공연 생성 서비스 로직 생성 * [#50] feat(PerformanceController): 공연 생성 POST API 생성 --- .../performance/api/HomeController.java | 4 +- .../api/PerformanceController.java | 17 ++ .../application/PerformanceCreateService.java | 155 ++++++++++++++++++ .../application/PerformanceService.java | 4 + .../application/dto/create/CastRequest.java | 7 + .../application/dto/create/CastResponse.java | 22 +++ .../dto/create/PerformanceRequest.java | 27 +++ .../dto/create/PerformanceResponse.java | 75 +++++++++ .../dto/create/ScheduleRequest.java | 11 ++ .../dto/create/ScheduleResponse.java | 29 ++++ .../application/dto/create/StaffRequest.java | 7 + .../application/dto/create/StaffResponse.java | 22 +++ .../dto/{ => home}/HomePerformanceDetail.java | 2 +- .../dto/{ => home}/HomePromotionDetail.java | 2 +- .../dto/{ => home}/HomeRequest.java | 2 +- .../dto/{ => home}/HomeResponse.java | 2 +- .../exception/PerformanceSuccessCode.java | 1 + 17 files changed, 383 insertions(+), 6 deletions(-) create mode 100644 src/main/java/com/beat/domain/performance/application/PerformanceCreateService.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/CastRequest.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/CastResponse.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/PerformanceRequest.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/PerformanceResponse.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/ScheduleRequest.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/ScheduleResponse.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/StaffRequest.java create mode 100644 src/main/java/com/beat/domain/performance/application/dto/create/StaffResponse.java rename src/main/java/com/beat/domain/performance/application/dto/{ => home}/HomePerformanceDetail.java (91%) rename src/main/java/com/beat/domain/performance/application/dto/{ => home}/HomePromotionDetail.java (84%) rename src/main/java/com/beat/domain/performance/application/dto/{ => home}/HomeRequest.java (61%) rename src/main/java/com/beat/domain/performance/application/dto/{ => home}/HomeResponse.java (85%) diff --git a/src/main/java/com/beat/domain/performance/api/HomeController.java b/src/main/java/com/beat/domain/performance/api/HomeController.java index 9bb7abe2..7a7a7159 100644 --- a/src/main/java/com/beat/domain/performance/api/HomeController.java +++ b/src/main/java/com/beat/domain/performance/api/HomeController.java @@ -1,8 +1,8 @@ package com.beat.domain.performance.api; import com.beat.domain.performance.application.PerformanceService; -import com.beat.domain.performance.application.dto.HomeRequest; -import com.beat.domain.performance.application.dto.HomeResponse; +import com.beat.domain.performance.application.dto.home.HomeRequest; +import com.beat.domain.performance.application.dto.home.HomeResponse; import com.beat.domain.performance.domain.Genre; import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.global.common.dto.SuccessResponse; diff --git a/src/main/java/com/beat/domain/performance/api/PerformanceController.java b/src/main/java/com/beat/domain/performance/api/PerformanceController.java index 1e54ae85..72c685d9 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceController.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -1,17 +1,23 @@ package com.beat.domain.performance.api; +import com.beat.domain.performance.application.PerformanceCreateService; import com.beat.domain.performance.application.dto.BookingPerformanceDetailResponse; import com.beat.domain.performance.application.dto.MakerPerformanceResponse; import com.beat.domain.performance.application.dto.PerformanceDetailResponse; +import com.beat.domain.performance.application.dto.create.PerformanceRequest; +import com.beat.domain.performance.application.dto.create.PerformanceResponse; import com.beat.domain.performance.exception.PerformanceSuccessCode; import com.beat.domain.performance.application.PerformanceService; import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.SuccessResponse; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @@ -21,6 +27,17 @@ public class PerformanceController { private final PerformanceService performanceService; + private final PerformanceCreateService performanceCreateService; + + @Operation(summary = "공연 생성 API", description = "공연을 생성하는 POST API입니다.") + @PostMapping + public ResponseEntity> createPerformance( + @CurrentMember Long userId, + @RequestBody PerformanceRequest performanceRequest) { + PerformanceResponse response = performanceCreateService.createPerformance(userId, performanceRequest); + return ResponseEntity.status(HttpStatus.CREATED) + .body(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_CREATE_SUCCESS, response)); + } @Operation(summary = "공연 상세정보 조회 API", description = "공연 상세페이지의 공연 상세정보를 조회하는 GET API입니다.") @GetMapping("/detail/{performanceId}") diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceCreateService.java b/src/main/java/com/beat/domain/performance/application/PerformanceCreateService.java new file mode 100644 index 00000000..82e06031 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/PerformanceCreateService.java @@ -0,0 +1,155 @@ +package com.beat.domain.performance.application; + +import com.beat.domain.cast.dao.CastRepository; +import com.beat.domain.cast.domain.Cast; +import com.beat.domain.performance.application.dto.create.CastResponse; +import com.beat.domain.performance.application.dto.create.PerformanceRequest; +import com.beat.domain.performance.application.dto.create.PerformanceResponse; +import com.beat.domain.performance.application.dto.create.ScheduleResponse; +import com.beat.domain.performance.application.dto.create.StaffResponse; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import com.beat.domain.staff.dao.StaffRepository; +import com.beat.domain.staff.domain.Staff; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; +import com.beat.global.common.exception.NotFoundException; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +@Service +@RequiredArgsConstructor +public class PerformanceCreateService { + + private final PerformanceRepository performanceRepository; + private final ScheduleRepository scheduleRepository; + private final UserRepository userRepository; + private final CastRepository castRepository; + private final StaffRepository staffRepository; + + @Transactional + public PerformanceResponse createPerformance(Long userId, PerformanceRequest request) { + Users user = userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + + Performance performance = Performance.create( + request.performanceTitle(), + request.genre(), + request.runningTime(), + request.performanceDescription(), + request.performanceAttentionNote(), + request.bankName(), + request.accountNumber(), + request.accountHolder(), + request.posterImage(), + request.performanceTeamName(), + request.performanceVenue(), + request.performanceContact(), + request.performancePeriod(), + request.ticketPrice(), + request.totalScheduleCount(), + user + ); + performanceRepository.save(performance); + + List schedules = request.scheduleList().stream() + .map(scheduleRequest -> Schedule.create( + scheduleRequest.performanceDate(), + scheduleRequest.totalTicketCount(), + 0, + true, + scheduleRequest.scheduleNumber(), + performance + )) + .collect(Collectors.toList()); + scheduleRepository.saveAll(schedules); + + List casts = request.castList().stream() + .map(castRequest -> Cast.create( + castRequest.castName(), + castRequest.castRole(), + castRequest.castPhoto(), + performance + )) + .collect(Collectors.toList()); + castRepository.saveAll(casts); + + List staffs = request.staffList().stream() + .map(staffRequest -> Staff.create( + staffRequest.staffName(), + staffRequest.staffRole(), + staffRequest.staffPhoto(), + performance + )) + .collect(Collectors.toList()); + staffRepository.saveAll(staffs); + + return mapToPerformanceResponse(performance, schedules, casts, staffs); + } + + private PerformanceResponse mapToPerformanceResponse(Performance performance, List schedules, List casts, List staffs) { + List scheduleResponses = schedules.stream() + .map(schedule -> ScheduleResponse.of( + schedule.getId(), + schedule.getPerformanceDate(), + schedule.getTotalTicketCount(), + calculateDueDate(schedule.getPerformanceDate().toLocalDate()), + schedule.getScheduleNumber() + )) + .collect(Collectors.toList()); + + List castResponses = casts.stream() + .map(cast -> CastResponse.of( + cast.getId(), + cast.getCastName(), + cast.getCastRole(), + cast.getCastPhoto() + )) + .collect(Collectors.toList()); + + List staffResponses = staffs.stream() + .map(staff -> StaffResponse.of( + staff.getId(), + staff.getStaffName(), + staff.getStaffRole(), + staff.getStaffPhoto() + )) + .collect(Collectors.toList()); + + return PerformanceResponse.of( + performance.getUsers().getId(), + performance.getId(), + performance.getPerformanceTitle(), + performance.getGenre(), + performance.getRunningTime(), + performance.getPerformanceDescription(), + performance.getPerformanceAttentionNote(), + performance.getBankName(), + performance.getAccountNumber(), + performance.getAccountHolder(), + performance.getPosterImage(), + performance.getPerformanceTeamName(), + performance.getPerformanceVenue(), + performance.getPerformanceContact(), + performance.getPerformancePeriod(), + performance.getTicketPrice(), + performance.getTotalScheduleCount(), + scheduleResponses, + castResponses, + staffResponses + ); + } + + private int calculateDueDate(LocalDate performanceDate) { + return (int) ChronoUnit.DAYS.between(LocalDate.now(), performanceDate); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceService.java b/src/main/java/com/beat/domain/performance/application/PerformanceService.java index 62136665..ff96a175 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -3,6 +3,10 @@ import com.beat.domain.member.domain.Member; import com.beat.domain.member.exception.MemberErrorCode; import com.beat.domain.performance.application.dto.*; +import com.beat.domain.performance.application.dto.home.HomePerformanceDetail; +import com.beat.domain.performance.application.dto.home.HomePromotionDetail; +import com.beat.domain.performance.application.dto.home.HomeRequest; +import com.beat.domain.performance.application.dto.home.HomeResponse; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; import com.beat.domain.performance.exception.PerformanceErrorCode; diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/CastRequest.java b/src/main/java/com/beat/domain/performance/application/dto/create/CastRequest.java new file mode 100644 index 00000000..60cc9b47 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/CastRequest.java @@ -0,0 +1,7 @@ +package com.beat.domain.performance.application.dto.create; + +public record CastRequest( + String castName, + String castRole, + String castPhoto +) {} diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/CastResponse.java b/src/main/java/com/beat/domain/performance/application/dto/create/CastResponse.java new file mode 100644 index 00000000..00f18e4c --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/CastResponse.java @@ -0,0 +1,22 @@ +package com.beat.domain.performance.application.dto.create; + +public record CastResponse( + Long castId, + String castName, + String castRole, + String castPhoto +) { + public static CastResponse of( + Long castId, + String castName, + String castRole, + String castPhoto + ) { + return new CastResponse( + castId, + castName, + castRole, + castPhoto + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceRequest.java b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceRequest.java new file mode 100644 index 00000000..1ae592f9 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceRequest.java @@ -0,0 +1,27 @@ +package com.beat.domain.performance.application.dto.create; + +import com.beat.domain.performance.domain.BankName; +import com.beat.domain.performance.domain.Genre; + +import java.util.List; + +public record PerformanceRequest( + String performanceTitle, + Genre genre, + int runningTime, + String performanceDescription, + String performanceAttentionNote, + BankName bankName, + String accountNumber, + String accountHolder, + String posterImage, + String performanceTeamName, + String performanceVenue, + String performanceContact, + String performancePeriod, + int ticketPrice, + int totalScheduleCount, + List scheduleList, + List castList, + List staffList +) {} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceResponse.java b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceResponse.java new file mode 100644 index 00000000..9d34b8da --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceResponse.java @@ -0,0 +1,75 @@ +package com.beat.domain.performance.application.dto.create; + +import com.beat.domain.performance.domain.BankName; +import com.beat.domain.performance.domain.Genre; + +import java.util.List; + +public record PerformanceResponse( + Long userId, + Long performanceId, + String performanceTitle, + Genre genre, + int runningTime, + String performanceDescription, + String performanceAttentionNote, + BankName bankName, + String accountNumber, + String accountHolder, + String posterImage, + String performanceTeamName, + String performanceVenue, + String performanceContact, + String performancePeriod, + int ticketPrice, + int totalScheduleCount, + List scheduleList, + List castList, + List staffList +) { + public static PerformanceResponse of( + Long userId, + Long performanceId, + String performanceTitle, + Genre genre, + int runningTime, + String performanceDescription, + String performanceAttentionNote, + BankName bankName, + String accountNumber, + String accountHolder, + String posterImage, + String performanceTeamName, + String performanceVenue, + String performanceContact, + String performancePeriod, + int ticketPrice, + int totalScheduleCount, + List scheduleList, + List castList, + List staffList + ) { + return new PerformanceResponse( + userId, + performanceId, + performanceTitle, + genre, + runningTime, + performanceDescription, + performanceAttentionNote, + bankName, + accountNumber, + accountHolder, + posterImage, + performanceTeamName, + performanceVenue, + performanceContact, + performancePeriod, + ticketPrice, + totalScheduleCount, + scheduleList, + castList, + staffList + ); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleRequest.java b/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleRequest.java new file mode 100644 index 00000000..526d0d6e --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleRequest.java @@ -0,0 +1,11 @@ +package com.beat.domain.performance.application.dto.create; + +import com.beat.domain.schedule.domain.ScheduleNumber; + +import java.time.LocalDateTime; + +public record ScheduleRequest( + LocalDateTime performanceDate, + int totalTicketCount, + ScheduleNumber scheduleNumber +) {} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleResponse.java b/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleResponse.java new file mode 100644 index 00000000..88a06f46 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/ScheduleResponse.java @@ -0,0 +1,29 @@ +package com.beat.domain.performance.application.dto.create; + +import com.beat.domain.schedule.domain.ScheduleNumber; + +import java.time.LocalDateTime; + +public record ScheduleResponse( + Long scheduleId, + LocalDateTime performanceDate, + int totalTicketCount, + int dueDate, + ScheduleNumber scheduleNumber +) { + public static ScheduleResponse of( + Long scheduleId, + LocalDateTime performanceDate, + int totalTicketCount, + int dueDate, + ScheduleNumber scheduleNumber + ) { + return new ScheduleResponse( + scheduleId, + performanceDate, + totalTicketCount, + dueDate, + scheduleNumber + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/StaffRequest.java b/src/main/java/com/beat/domain/performance/application/dto/create/StaffRequest.java new file mode 100644 index 00000000..8b126d56 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/StaffRequest.java @@ -0,0 +1,7 @@ +package com.beat.domain.performance.application.dto.create; + +public record StaffRequest( + String staffName, + String staffRole, + String staffPhoto +) {} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/StaffResponse.java b/src/main/java/com/beat/domain/performance/application/dto/create/StaffResponse.java new file mode 100644 index 00000000..c08fc2f9 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/StaffResponse.java @@ -0,0 +1,22 @@ +package com.beat.domain.performance.application.dto.create; + +public record StaffResponse( + Long staffId, + String staffName, + String staffRole, + String staffPhoto +) { + public static StaffResponse of( + Long staffId, + String staffName, + String staffRole, + String staffPhoto + ) { + return new StaffResponse( + staffId, + staffName, + staffRole, + staffPhoto + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java similarity index 91% rename from src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java rename to src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java index 51537a79..1417cfb4 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/HomePerformanceDetail.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java @@ -1,4 +1,4 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.home; public record HomePerformanceDetail( Long performanceId, diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java similarity index 84% rename from src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java rename to src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java index 02ead354..9bd9e35f 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/HomePromotionDetail.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java @@ -1,4 +1,4 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.home; public record HomePromotionDetail( Long promotionId, diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java similarity index 61% rename from src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java rename to src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java index 518eae89..841c8f08 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/HomeRequest.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java @@ -1,4 +1,4 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.home; import com.beat.domain.performance.domain.Genre; diff --git a/src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java similarity index 85% rename from src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java rename to src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java index 7690d1c4..c3d963b4 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/HomeResponse.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java @@ -1,4 +1,4 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.home; import java.util.List; diff --git a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java index 596b114f..83c51436 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceSuccessCode.java @@ -7,6 +7,7 @@ @Getter @RequiredArgsConstructor public enum PerformanceSuccessCode implements BaseSuccessCode { + PERFORMANCE_CREATE_SUCCESS(201, "공연이 성공정으로 생성되었습니다."), PERFORMANCE_RETRIEVE_SUCCESS(200, "공연 상세 정보 조회가 성공적으로 완료되었습니다."), BOOKING_PERFORMANCE_RETRIEVE_SUCCESS(200, "예매 관련 공연 정보 조회가 성공적으로 완료되었습니다."), HOME_PERFORMANCE_RETRIEVE_SUCCESS(200, "홈 화면 공연 목록 조회가 성공적으로 완료되었습니다."),