diff --git a/.github/workflows/dev-CI.yml b/.github/workflows/dev-CI.yml index 9e682ab2..3776a7c6 100644 --- a/.github/workflows/dev-CI.yml +++ b/.github/workflows/dev-CI.yml @@ -53,6 +53,10 @@ jobs: DEV_COOLSMS_KEY: ${{ secrets.DEV_COOLSMS_KEY }} DEV_COOLSMS_NUMBER: ${{ secrets.DEV_COOLSMS_NUMBER }} DEV_COOLSMS_SECRET: ${{ secrets.DEV_COOLSMS_SECRET }} + DEV_ACCESS_TOKEN_EXPIRE_TIME: ${{ secrets.DEV_ACCESS_TOKEN_EXPIRE_TIME }} + DEV_REFRESH_TOKEN_EXPIRE_TIME: ${{ secrets.DEV_REFRESH_TOKEN_EXPIRE_TIME }} + DEV_ALLOWED_ORIGINS: ${{ secrets.DEV_ALLOWED_ORIGINS }} + DEV_SERVER_URL: ${{ secrets.DEV_SERVER_URL }} run: | cd ./src/main/resources envsubst < application-dev.yml > application-dev.tmp.yml && mv application-dev.tmp.yml application-dev.yml @@ -81,11 +85,11 @@ jobs: docker build -f Dockerfile-dev --platform linux/amd64 -t hoonyworld/beat-dev . docker push hoonyworld/beat-dev - # Trigger Jenkins job - Jenkins 작업 트리거 - - name: Trigger Jenkins job - uses: appleboy/jenkins-action@master - with: - url: ${{ secrets.DEV_WEBHOOK_URL }} - user: "beat" - token: ${{ secrets.DEV_JENKINS_API_TOKEN }} - job: "beat-project" \ No newline at end of file +# # Trigger Jenkins job - Jenkins 작업 트리거 +# - name: Trigger Jenkins job +# uses: appleboy/jenkins-action@master +# with: +# url: ${{ secrets.DEV_WEBHOOK_URL }} +# user: "beat" +# token: ${{ secrets.DEV_JENKINS_API_TOKEN }} +# job: "beat-project" \ No newline at end of file diff --git a/.github/workflows/prod-CI.yml b/.github/workflows/prod-CI.yml index c478ff71..deed64ad 100644 --- a/.github/workflows/prod-CI.yml +++ b/.github/workflows/prod-CI.yml @@ -53,6 +53,10 @@ jobs: PROD_COOLSMS_KEY: ${{ secrets.PROD_COOLSMS_KEY }} PROD_COOLSMS_NUMBER: ${{ secrets.PROD_COOLSMS_NUMBER }} PROD_COOLSMS_SECRET: ${{ secrets.PROD_COOLSMS_SECRET }} + PROD_ACCESS_TOKEN_EXPIRE_TIME: ${{ secrets.PROD_ACCESS_TOKEN_EXPIRE_TIME }} + PROD_REFRESH_TOKEN_EXPIRE_TIME: ${{ secrets.PROD_REFRESH_TOKEN_EXPIRE_TIME }} + PROD_ALLOWED_ORIGINS: ${{ secrets.PROD_ALLOWED_ORIGINS }} + PROD_SERVER_URL: ${{ secrets.PROD_SERVER_URL }} run: | cd ./src/main/resources envsubst < application-prod.yml > application-prod.tmp.yml && mv application-prod.tmp.yml application-prod.yml @@ -81,11 +85,11 @@ jobs: docker build --platform linux/amd64 -t donghoon0203/beat-prod . docker push donghoon0203/beat-prod - # Trigger Jenkins job - Jenkins 작업 트리거 - - name: Trigger Jenkins job - uses: appleboy/jenkins-action@master - with: - url: ${{ secrets.PROD_WEBHOOK_URL }} - user: "beat" - token: ${{ secrets.PROD_JENKINS_API_TOKEN }} - job: "beat-project" +# # Trigger Jenkins job - Jenkins 작업 트리거 +# - name: Trigger Jenkins job +# uses: appleboy/jenkins-action@master +# with: +# url: ${{ secrets.PROD_WEBHOOK_URL }} +# user: "beat" +# token: ${{ secrets.PROD_JENKINS_API_TOKEN }} +# job: "beat-project" diff --git a/.gitignore b/.gitignore index e0586444..bf9eeb1d 100644 --- a/.gitignore +++ b/.gitignore @@ -178,3 +178,6 @@ Temporary Items # End of https://www.toptal.com/developers/gitignore/api/intellij,java,macos.gradle/ .idea/ + +# Ignore application-local.yml +src/main/resources/application-local.yml \ No newline at end of file diff --git a/HELP.md b/HELP.md deleted file mode 100644 index 8826d98e..00000000 --- a/HELP.md +++ /dev/null @@ -1,30 +0,0 @@ -# Getting Started - -### Reference Documentation -For further reference, please consider the following sections: - -* [Official Gradle documentation](https://docs.gradle.org) -* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/) -* [Create an OCI image](https://docs.spring.io/spring-boot/docs/3.3.1/gradle-plugin/reference/html/#build-image) -* [Spring Web](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#web) -* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#data.sql.jpa-and-spring-data) -* [Spring Security](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#web.security) -* [Spring Boot DevTools](https://docs.spring.io/spring-boot/docs/3.3.1/reference/htmlsingle/index.html#using.devtools) - -### Guides -The following guides illustrate how to use some features concretely: - -* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/) -* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/) -* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/) -* [Accessing data with MySQL](https://spring.io/guides/gs/accessing-data-mysql/) -* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/) -* [Securing a Web Application](https://spring.io/guides/gs/securing-web/) -* [Spring Boot and OAuth2](https://spring.io/guides/tutorials/spring-boot-oauth2/) -* [Authenticating a User with LDAP](https://spring.io/guides/gs/authenticating-ldap/) - -### Additional Links -These additional references should also help you: - -* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) - diff --git a/README.md b/README.md index 1329a49e..675db961 100644 --- a/README.md +++ b/README.md @@ -114,4 +114,4 @@ BEAT와 함께 효율적이고 체계적으로 공연을 관리해 볼까요? ## 👥 Contributors -- [BEAT Client Repository](https://github.com/TEAM-BEAT/BEAT-Client) \ No newline at end of file +- [BEAT Client Repository](https://github.com/TEAM-BEAT/BEAT-Client) diff --git a/build.gradle b/build.gradle index 82a3303b..dd665e45 100644 --- a/build.gradle +++ b/build.gradle @@ -28,7 +28,7 @@ repositories { dependencies { // Spring implementation 'org.springframework.boot:spring-boot-starter-web' - developmentOnly 'org.springframework.boot:spring-boot-devtools' +// developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-actuator' // Database diff --git a/src/main/java/com/beat/BeatApplication.java b/src/main/java/com/beat/BeatApplication.java index 263f8261..da272990 100644 --- a/src/main/java/com/beat/BeatApplication.java +++ b/src/main/java/com/beat/BeatApplication.java @@ -5,9 +5,13 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.annotation.EnableScheduling; @SpringBootApplication @EnableFeignClients +@EnableScheduling +@EnableAsync @ImportAutoConfiguration({FeignAutoConfiguration.class}) public class BeatApplication { diff --git a/src/main/java/com/beat/admin/adapter/in/api/AdminApi.java b/src/main/java/com/beat/admin/adapter/in/api/AdminApi.java new file mode 100644 index 00000000..f5fb2ea6 --- /dev/null +++ b/src/main/java/com/beat/admin/adapter/in/api/AdminApi.java @@ -0,0 +1,134 @@ +package com.beat.admin.adapter.in.api; + +import com.beat.admin.application.dto.response.CarouselFindAllResponse; +import com.beat.admin.application.dto.response.UserFindAllResponse; +import com.beat.admin.application.dto.request.CarouselHandleRequest; +import com.beat.admin.application.dto.response.CarouselHandleAllResponse; +import com.beat.global.auth.annotation.CurrentMember; +import com.beat.global.common.dto.ErrorResponse; +import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.external.s3.application.dto.BannerPresignedUrlFindResponse; +import com.beat.global.external.s3.application.dto.CarouselPresignedUrlFindAllResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Tag(name = "Admin", description = "관리자 제어 API") +public interface AdminApi { + + @Operation(summary = "유저 정보 조회", description = "관리자가 유저들의 정보를 조회하는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "관리자 권한으로 모든 유저 조회에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "회원이 없습니다", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> readAllUsers( + @CurrentMember Long memberId + ); + + @Operation(summary = "캐러셀에 업로드 할 이미지에 대한 presigned URL 발급", description = "관리자가 캐러셀에 업로드 할 이미지에 대한 presigned URL을 발급 받는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "캐러셀 Presigned URL 발급 성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "회원이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> createAllCarouselPresignedUrls( + @CurrentMember Long memberId, + @RequestParam List carouselImages + ); + + @Operation(summary = "배너에 업로드 할 이미지에 대한 presigned URL 발급", description = "관리자가 배너에 업로드 할 이미지에 대한 presigned URL을 발급 받는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "배너 Presigned URL 발급 성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "회원이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> createBannerPresignedUrl( + @CurrentMember Long memberId, + @RequestParam String bannerImage + ); + + @Operation(summary = "캐러셀에 등록된 모든 공연 정보 조회", description = "관리자가 현재 캐러셀에 등록된 모든 공연 정보를 조회하는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "관리자 권한으로 현재 캐러셀에 등록된 모든 공연 조회에 성공하였습니다.", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "회원이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> readAllCarouselImages( + @CurrentMember Long memberId + ); + + @Operation(summary = "캐러셀 이미지 수정", description = "관리자가 캐러셀 이미지를 수정하는 PUT API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "캐러셀 이미지 수정 성공", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "회원이 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "해당 홍보 정보를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ), + @ApiResponse( + responseCode = "404", + description = "해당 공연 정보를 찾을 수 없습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> processCarouselImages( + @CurrentMember Long memberId, + @RequestBody CarouselHandleRequest request + ); +} diff --git a/src/main/java/com/beat/admin/adapter/in/api/AdminController.java b/src/main/java/com/beat/admin/adapter/in/api/AdminController.java new file mode 100644 index 00000000..20dd1f31 --- /dev/null +++ b/src/main/java/com/beat/admin/adapter/in/api/AdminController.java @@ -0,0 +1,79 @@ +package com.beat.admin.adapter.in.api; + +import com.beat.admin.application.dto.response.CarouselFindAllResponse; +import com.beat.admin.application.dto.request.CarouselHandleRequest; +import com.beat.admin.application.dto.response.CarouselHandleAllResponse; +import com.beat.admin.exception.AdminSuccessCode; +import com.beat.admin.application.dto.response.UserFindAllResponse; +import com.beat.admin.facade.AdminFacade; +import com.beat.global.auth.annotation.CurrentMember; +import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.external.s3.application.dto.BannerPresignedUrlFindResponse; +import com.beat.global.external.s3.application.dto.CarouselPresignedUrlFindAllResponse; + +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.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequestMapping("/api/admin") +@RequiredArgsConstructor +public class AdminController implements AdminApi { + + private final AdminFacade adminFacade; + + @Override + @GetMapping("/users") + public ResponseEntity> readAllUsers(@CurrentMember Long memberId) { + UserFindAllResponse response = adminFacade.checkMemberAndFindAllUsers(memberId); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(AdminSuccessCode.FETCH_ALL_USERS_SUCCESS, response)); + } + + @Override + @GetMapping("/carousels/presigned-url") + public ResponseEntity> createAllCarouselPresignedUrls( + @CurrentMember Long memberId, @RequestParam List carouselImages) { + CarouselPresignedUrlFindAllResponse response = adminFacade.checkMemberAndIssueAllPresignedUrlsForCarousel( + memberId, carouselImages); + return ResponseEntity.ok(SuccessResponse.of(AdminSuccessCode.CAROUSEL_PRESIGNED_URL_ISSUED, response)); + } + + @Override + @GetMapping("/banner/presigned-url") + public ResponseEntity> createBannerPresignedUrl( + @CurrentMember Long memberId, @RequestParam String bannerImage) { + BannerPresignedUrlFindResponse response = adminFacade.checkMemberAndIssuePresignedUrlForBanner(memberId, + bannerImage); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(AdminSuccessCode.BANNER_PRESIGNED_URL_ISSUED, response)); + } + + @Override + @GetMapping("/carousels") + public ResponseEntity> readAllCarouselImages( + @CurrentMember Long memberId) { + CarouselFindAllResponse response = adminFacade.checkMemberAndFindAllPromotionsSortedByCarouselNumber(memberId); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(AdminSuccessCode.FETCH_ALL_CAROUSEL_PROMOTIONS_SUCCESS, response)); + } + + @Override + @PutMapping("/carousels") + public ResponseEntity> processCarouselImages( + @CurrentMember Long memberId, + @RequestBody CarouselHandleRequest request) { + CarouselHandleAllResponse response = adminFacade.checkMemberAndProcessAllPromotionsSortedByCarouselNumber(memberId, request); + return ResponseEntity.status(HttpStatus.OK) + .body(SuccessResponse.of(AdminSuccessCode.UPDATE_ALL_CAROUSEL_PROMOTIONS_SUCCESS, response)); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/AdminService.java b/src/main/java/com/beat/admin/application/AdminService.java new file mode 100644 index 00000000..530e79ec --- /dev/null +++ b/src/main/java/com/beat/admin/application/AdminService.java @@ -0,0 +1,89 @@ +package com.beat.admin.application; + +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionGenerateRequest; +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionModifyRequest; +import com.beat.admin.port.in.AdminUseCase; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.port.in.PerformanceUseCase; +import com.beat.domain.promotion.domain.Promotion; +import com.beat.domain.promotion.port.in.PromotionUseCase; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Optional; + +@Service +@RequiredArgsConstructor +public class AdminService implements AdminUseCase { + + private final PromotionUseCase promotionUseCase; + private final PerformanceUseCase performanceUseCase; + + @Override + @Transactional(readOnly = true) + public List findAllPromotionsSortedByCarouselNumber() { + List promotions = promotionUseCase.findAllPromotions(); + return sortPromotionsByCarouselNumber(promotions); + } + + @Override + @Transactional + public List processPromotionsAndSortByPromotionId(List modifyRequests, + List generateRequests, List deletePromotionIds) { + + handlePromotionDeletion(deletePromotionIds); + List modifiedPromotions = handlePromotionModification(modifyRequests); + List addedPromotions = handlePromotionGeneration(generateRequests); + + List applyPromotionChanges = new ArrayList<>(modifiedPromotions); + applyPromotionChanges.addAll(addedPromotions); + + return sortPromotionsByCarouselNumber(applyPromotionChanges); + } + + private void handlePromotionDeletion(List deletePromotionIds) { + if (!deletePromotionIds.isEmpty()) { + promotionUseCase.deletePromotionsByPromotionIds(deletePromotionIds); + } + } + + private List handlePromotionModification(List modifyRequests) { + return modifyRequests.stream() + .map(modifyRequest -> { + + Promotion promotion = promotionUseCase.findById(modifyRequest.promotionId()); + + Performance performance = Optional.ofNullable(modifyRequest.performanceId()) + .map(performanceUseCase::findById) + .orElse(null); + + return promotionUseCase.modifyPromotion(promotion, performance, modifyRequest); + }) + .toList(); + } + + private List handlePromotionGeneration(List generateRequests) { + return generateRequests.stream() + .map(generateRequest -> { + Performance performance = Optional.ofNullable(generateRequest.performanceId()) + .map(performanceUseCase::findById) + .orElse(null); + + return promotionUseCase.createPromotion(generateRequest.newImageUrl(), performance, + generateRequest.redirectUrl(), generateRequest.isExternal(), generateRequest.carouselNumber()); + }) + .toList(); + } + + private List sortPromotionsByCarouselNumber(List promotions) { + return promotions.stream() + .sorted(Comparator.comparing(Promotion::getCarouselNumber, Comparator.comparingInt(Enum::ordinal))) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/dto/request/CarouselHandleRequest.java b/src/main/java/com/beat/admin/application/dto/request/CarouselHandleRequest.java new file mode 100644 index 00000000..c5aa18a7 --- /dev/null +++ b/src/main/java/com/beat/admin/application/dto/request/CarouselHandleRequest.java @@ -0,0 +1,29 @@ +package com.beat.admin.application.dto.request; + +import java.util.List; + +import com.beat.domain.promotion.domain.CarouselNumber; + +public record CarouselHandleRequest( + List carousels +) { + + public record PromotionModifyRequest( + Long promotionId, + CarouselNumber carouselNumber, + String newImageUrl, + boolean isExternal, + String redirectUrl, + Long performanceId + ) implements PromotionHandleRequest { + } + + public record PromotionGenerateRequest( + CarouselNumber carouselNumber, + String newImageUrl, + boolean isExternal, + String redirectUrl, + Long performanceId + ) implements PromotionHandleRequest { + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/dto/request/PromotionHandleRequest.java b/src/main/java/com/beat/admin/application/dto/request/PromotionHandleRequest.java new file mode 100644 index 00000000..e293d603 --- /dev/null +++ b/src/main/java/com/beat/admin/application/dto/request/PromotionHandleRequest.java @@ -0,0 +1,28 @@ +package com.beat.admin.application.dto.request; + +import static com.beat.admin.application.dto.request.CarouselHandleRequest.*; + +import com.beat.domain.promotion.domain.CarouselNumber; +import com.fasterxml.jackson.annotation.JsonSubTypes; +import com.fasterxml.jackson.annotation.JsonTypeInfo; + +@JsonTypeInfo( + use = JsonTypeInfo.Id.NAME, + property = "type" +) +@JsonSubTypes({ + @JsonSubTypes.Type(value = PromotionModifyRequest.class, name = "modify"), + @JsonSubTypes.Type(value = PromotionGenerateRequest.class, name = "generate") +}) +public sealed interface PromotionHandleRequest + permits PromotionModifyRequest, PromotionGenerateRequest { + CarouselNumber carouselNumber(); + + String newImageUrl(); + + boolean isExternal(); + + String redirectUrl(); + + Long performanceId(); +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/dto/response/CarouselFindAllResponse.java b/src/main/java/com/beat/admin/application/dto/response/CarouselFindAllResponse.java new file mode 100644 index 00000000..878aa861 --- /dev/null +++ b/src/main/java/com/beat/admin/application/dto/response/CarouselFindAllResponse.java @@ -0,0 +1,41 @@ +package com.beat.admin.application.dto.response; + +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.promotion.domain.CarouselNumber; +import com.beat.domain.promotion.domain.Promotion; + +import java.util.List; +import java.util.Optional; + +public record CarouselFindAllResponse( + List carousels +) { + public static CarouselFindAllResponse from(List promotions) { + List responses = promotions.stream() + .map(CarouselFindResponse::from) + .toList(); + return new CarouselFindAllResponse(responses); + } + + public record CarouselFindResponse( + Long promotionId, + CarouselNumber carouselNumber, + String newImageUrl, + boolean isExternal, + String redirectUrl, + Long performanceId + ) { + public static CarouselFindResponse from(Promotion promotion) { + return new CarouselFindResponse( + promotion.getId(), + promotion.getCarouselNumber(), + promotion.getPromotionPhoto(), + promotion.isExternal(), + promotion.getRedirectUrl(), + Optional.ofNullable(promotion.getPerformance()) + .map(Performance::getId) + .orElse(null) + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/dto/response/CarouselHandleAllResponse.java b/src/main/java/com/beat/admin/application/dto/response/CarouselHandleAllResponse.java new file mode 100644 index 00000000..999224b0 --- /dev/null +++ b/src/main/java/com/beat/admin/application/dto/response/CarouselHandleAllResponse.java @@ -0,0 +1,36 @@ +package com.beat.admin.application.dto.response; + +import java.util.List; +import java.util.stream.Collectors; + +import com.beat.domain.promotion.domain.Promotion; + +public record CarouselHandleAllResponse( + List modifiedPromotions +) { + + public static CarouselHandleAllResponse from(List promotions) { + List modifiedPromotions = promotions.stream() + .map(PromotionResponse::from) + .collect(Collectors.toList()); + return new CarouselHandleAllResponse(modifiedPromotions); + } + + public record PromotionResponse( + Long promotionId, + String newImageUrl, + boolean isExternal, + String redirectUrl, + String carouselNumber + ) { + public static PromotionResponse from(Promotion promotion) { + return new PromotionResponse( + promotion.getId(), + promotion.getPromotionPhoto(), + promotion.isExternal(), + promotion.getRedirectUrl(), + promotion.getCarouselNumber().name() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/application/dto/response/UserFindAllResponse.java b/src/main/java/com/beat/admin/application/dto/response/UserFindAllResponse.java new file mode 100644 index 00000000..a5274131 --- /dev/null +++ b/src/main/java/com/beat/admin/application/dto/response/UserFindAllResponse.java @@ -0,0 +1,28 @@ +package com.beat.admin.application.dto.response; + +import com.beat.domain.user.domain.Users; + +import java.util.List; + +public record UserFindAllResponse( + List users +) { + public static UserFindAllResponse from(List users) { + List userFindResponses = users.stream() + .map(UserFindResponse::from) + .toList(); + return new UserFindAllResponse(userFindResponses); + } + + public record UserFindResponse( + Long id, + String role + ) { + public static UserFindResponse from(Users user) { + return new UserFindResponse( + user.getId(), + user.getRole().getRoleName() + ); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/exception/AdminSuccessCode.java b/src/main/java/com/beat/admin/exception/AdminSuccessCode.java new file mode 100644 index 00000000..7125c6a3 --- /dev/null +++ b/src/main/java/com/beat/admin/exception/AdminSuccessCode.java @@ -0,0 +1,19 @@ +package com.beat.admin.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AdminSuccessCode implements BaseSuccessCode { + FETCH_ALL_USERS_SUCCESS(200, "관리자 권한으로 모든 유저 조회에 성공하였습니다."), + CAROUSEL_PRESIGNED_URL_ISSUED(200, "캐러셀 Presigned URL 발급 성공"), + BANNER_PRESIGNED_URL_ISSUED(200, "배너 Presigned URL 발급 성공"), + FETCH_ALL_CAROUSEL_PROMOTIONS_SUCCESS(200, "관리자 권한으로 현재 캐러셀에 등록된 모든 공연 조회에 성공하였습니다."), + UPDATE_ALL_CAROUSEL_PROMOTIONS_SUCCESS(200, "관리자 권한으로 캐러셀 수정에 성공하였습니다.") + ; + + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/facade/AdminFacade.java b/src/main/java/com/beat/admin/facade/AdminFacade.java new file mode 100644 index 00000000..968b1d5b --- /dev/null +++ b/src/main/java/com/beat/admin/facade/AdminFacade.java @@ -0,0 +1,111 @@ +package com.beat.admin.facade; + +import com.beat.admin.application.dto.request.PromotionHandleRequest; +import com.beat.admin.application.dto.response.CarouselFindAllResponse; +import com.beat.admin.application.dto.response.UserFindAllResponse; +import com.beat.admin.application.dto.request.CarouselHandleRequest; +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionGenerateRequest; +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionModifyRequest; +import com.beat.admin.application.dto.response.CarouselHandleAllResponse; +import com.beat.admin.port.in.AdminUseCase; +import com.beat.domain.member.port.in.MemberUseCase; +import com.beat.domain.promotion.domain.Promotion; +import com.beat.domain.promotion.port.in.PromotionUseCase; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.port.in.UserUseCase; +import com.beat.global.external.s3.application.dto.BannerPresignedUrlFindResponse; +import com.beat.global.external.s3.application.dto.CarouselPresignedUrlFindAllResponse; +import com.beat.global.external.s3.port.in.FileUseCase; + +import lombok.RequiredArgsConstructor; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +@Service +@Transactional +@RequiredArgsConstructor +public class AdminFacade { + private final FileUseCase fileUseCase; + private final AdminUseCase adminUsecase; + private final MemberUseCase memberUseCase; + private final UserUseCase userUseCase; + private final PromotionUseCase promotionUseCase; + + public UserFindAllResponse checkMemberAndFindAllUsers(Long memberId) { + memberUseCase.findMemberByMemberId(memberId); + List users = userUseCase.findAllUsers(); + return UserFindAllResponse.from(users); + } + + public CarouselPresignedUrlFindAllResponse checkMemberAndIssueAllPresignedUrlsForCarousel(Long memberId, + List carouselImages) { + memberUseCase.findMemberByMemberId(memberId); + Map carouselPresignedUrls = fileUseCase.issueAllPresignedUrlsForCarousel(carouselImages); + return CarouselPresignedUrlFindAllResponse.from(carouselPresignedUrls); + } + + public BannerPresignedUrlFindResponse checkMemberAndIssuePresignedUrlForBanner(Long memberId, String bannerImage) { + memberUseCase.findMemberByMemberId(memberId); + String bannerPresignedUrl = fileUseCase.issuePresignedUrlForBanner(bannerImage); + return BannerPresignedUrlFindResponse.from(bannerPresignedUrl); + } + + public CarouselFindAllResponse checkMemberAndFindAllPromotionsSortedByCarouselNumber(Long memberId) { + memberUseCase.findMemberByMemberId(memberId); + List promotions = adminUsecase.findAllPromotionsSortedByCarouselNumber(); + return CarouselFindAllResponse.from(promotions); + } + + public CarouselHandleAllResponse checkMemberAndProcessAllPromotionsSortedByCarouselNumber(Long memberId, + CarouselHandleRequest request) { + + memberUseCase.findMemberByMemberId(memberId); + + List modifyRequests = new ArrayList<>(); + List generateRequests = new ArrayList<>(); + Set requestPromotionIds = new HashSet<>(); + + categorizePromotionRequestsByPromotionId(request, modifyRequests, generateRequests, requestPromotionIds); + + List allExistingPromotions = promotionUseCase.findAllPromotions(); + + List deletePromotionIds = extractDeletePromotionIds(allExistingPromotions, requestPromotionIds); + + List sortedPromotions = adminUsecase.processPromotionsAndSortByPromotionId(modifyRequests, + generateRequests, deletePromotionIds); + + return CarouselHandleAllResponse.from(sortedPromotions); + } + + private void categorizePromotionRequestsByPromotionId(CarouselHandleRequest request, + List modifyRequests, List generateRequests, + Set requestPromotionIds) { + + for (PromotionHandleRequest promotionRequest : request.carousels()) { + if (promotionRequest instanceof PromotionModifyRequest modifyRequest) { + modifyRequests.add(modifyRequest); + requestPromotionIds.add(modifyRequest.promotionId()); + } else if (promotionRequest instanceof PromotionGenerateRequest generateRequest) { + generateRequests.add(generateRequest); + } + } + } + + private List extractDeletePromotionIds(List allExistingPromotions, Set requestPromotionIds) { + Set allExistingPromotionIds = allExistingPromotions.stream() + .map(Promotion::getId) + .collect(Collectors.toSet()); + + return allExistingPromotionIds.stream() + .filter(existingId -> !requestPromotionIds.contains(existingId)) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/admin/port/in/AdminUseCase.java b/src/main/java/com/beat/admin/port/in/AdminUseCase.java new file mode 100644 index 00000000..93fa39fc --- /dev/null +++ b/src/main/java/com/beat/admin/port/in/AdminUseCase.java @@ -0,0 +1,14 @@ +package com.beat.admin.port.in; + +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionGenerateRequest; +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionModifyRequest; +import com.beat.domain.promotion.domain.Promotion; + +import java.util.List; + +public interface AdminUseCase { + List findAllPromotionsSortedByCarouselNumber(); + + List processPromotionsAndSortByPromotionId(List modifyRequests, + List generateRequests, List deletePromotionIds); +} \ No newline at end of file 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 3f54b30b..dae62784 100644 --- a/src/main/java/com/beat/domain/booking/api/BookingController.java +++ b/src/main/java/com/beat/domain/booking/api/BookingController.java @@ -47,7 +47,7 @@ public ResponseEntity> createMemberBookin @Operation(summary = "회원 예매 조회 API", description = "회원이 예매를 조회하는 GET API입니다.") @GetMapping("/member/retrieve") - public ResponseEntity> getMemberBookings( + public ResponseEntity>> getMemberBookings( @CurrentMember Long memberId) { List response = memberBookingRetrieveService.findMemberBookings(memberId); return ResponseEntity.status(HttpStatus.OK) diff --git a/src/main/java/com/beat/domain/booking/api/TicketController.java b/src/main/java/com/beat/domain/booking/api/TicketController.java index 1e03e66e..ef10a3d7 100644 --- a/src/main/java/com/beat/domain/booking/api/TicketController.java +++ b/src/main/java/com/beat/domain/booking/api/TicketController.java @@ -1,9 +1,10 @@ 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.TicketCancelRequest; import com.beat.domain.booking.application.dto.TicketRetrieveResponse; import com.beat.domain.booking.application.dto.TicketUpdateRequest; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.booking.exception.BookingSuccessCode; import com.beat.global.auth.annotation.CurrentMember; import com.beat.global.common.dto.SuccessResponse; @@ -26,8 +27,8 @@ 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); + @RequestParam(required = false) BookingStatus bookingStatus) { + TicketRetrieveResponse response = ticketService.getTickets(memberId, performanceId, scheduleNumber, bookingStatus); return ResponseEntity.ok(SuccessResponse.of(BookingSuccessCode.TICKET_RETRIEVE_SUCCESS, response)); } @@ -37,15 +38,15 @@ public ResponseEntity> updateTickets( @CurrentMember Long memberId, @RequestBody TicketUpdateRequest request) { ticketService.updateTickets(memberId, request); - return ResponseEntity.ok(SuccessResponse.of(BookingSuccessCode.TICKET_UPDATE_SUCCESS, null)); + return ResponseEntity.ok(SuccessResponse.from(BookingSuccessCode.TICKET_UPDATE_SUCCESS)); } - @Operation(summary = "예매자 삭제 API", description = "메이커가 자신의 공연에 대한 예매자의 정보를 삭제하는 DELETE API입니다.") - @DeleteMapping - public ResponseEntity> deleteTickets( + @Operation(summary = "예매자 취소 API", description = "메이커가 자신의 공연에 대한 1명 이상의 예매자의 정보를 취소 상태로 변경하는 PATCH API입니다.") + @PatchMapping + public ResponseEntity> cancelTickets( @CurrentMember Long memberId, - @RequestBody TicketDeleteRequest ticketDeleteRequest) { - ticketService.deleteTickets(memberId, ticketDeleteRequest); - return ResponseEntity.ok(SuccessResponse.from(BookingSuccessCode.TICKET_DELETE_SUCCESS)); + @RequestBody TicketCancelRequest ticketCancelRequest) { + ticketService.cancelTickets(memberId, ticketCancelRequest); + return ResponseEntity.ok(SuccessResponse.from(BookingSuccessCode.TICKET_CANCEL_SUCCESS)); } } diff --git a/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java b/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java index 8a3aaa24..831d796b 100644 --- a/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingRetrieveService.java @@ -83,7 +83,7 @@ private GuestBookingRetrieveResponse toBookingResponse(Booking booking) { performance.getAccountNumber(), performance.getAccountHolder(), calculateDueDate(schedule.getPerformanceDate()), - booking.isPaymentCompleted(), + booking.getBookingStatus(), booking.getCreatedAt(), performance.getPosterImage(), totalPaymentAmount 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 f15601c8..0aee93f7 100644 --- a/src/main/java/com/beat/domain/booking/application/GuestBookingService.java +++ b/src/main/java/com/beat/domain/booking/application/GuestBookingService.java @@ -12,17 +12,15 @@ 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 lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @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; @@ -37,7 +35,7 @@ public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingR throw new BadRequestException(ScheduleErrorCode.INSUFFICIENT_TICKETS); } - schedule.setSoldTicketCount(schedule.getSoldTicketCount() + guestBookingRequest.purchaseTicketCount()); + updateSoldTicketCountAndIsBooking(schedule, guestBookingRequest.purchaseTicketCount()); Users users = bookingRepository.findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPassword( guestBookingRequest.bookerName(), @@ -58,7 +56,7 @@ public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingR guestBookingRequest.purchaseTicketCount(), guestBookingRequest.bookerName(), guestBookingRequest.bookerPhoneNumber(), - guestBookingRequest.isPaymentCompleted(), + guestBookingRequest.bookingStatus(), guestBookingRequest.birthDate(), guestBookingRequest.password(), schedule, @@ -66,7 +64,7 @@ public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingR ); bookingRepository.save(booking); - logger.info("Booking created: {}", booking); + log.info("Guest Booking created: {}", booking); return GuestBookingResponse.of( booking.getId(), @@ -76,11 +74,19 @@ public GuestBookingResponse createGuestBooking(GuestBookingRequest guestBookingR schedule.getScheduleNumber(), booking.getBookerName(), booking.getBookerPhoneNumber(), - booking.isPaymentCompleted(), + booking.getBookingStatus(), schedule.getPerformance().getBankName(), schedule.getPerformance().getAccountNumber(), totalPaymentAmount, booking.getCreatedAt() ); } + + private void updateSoldTicketCountAndIsBooking(Schedule schedule, int purchaseTicketCount) { + schedule.setSoldTicketCount(schedule.getSoldTicketCount() + purchaseTicketCount); + + if (schedule.getTotalTicketCount() == schedule.getSoldTicketCount()) { + schedule.updateIsBooking(false); + } + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java b/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java index 82c6981c..a12f20ec 100644 --- a/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java +++ b/src/main/java/com/beat/domain/booking/application/MemberBookingRetrieveService.java @@ -64,7 +64,7 @@ private MemberBookingRetrieveResponse toMemberBookingResponse(Booking booking) { performance.getAccountNumber(), performance.getAccountHolder(), calculateDueDate(schedule.getPerformanceDate()), - booking.isPaymentCompleted(), + booking.getBookingStatus(), booking.getCreatedAt(), performance.getPosterImage(), totalPaymentAmount diff --git a/src/main/java/com/beat/domain/booking/application/MemberBookingService.java b/src/main/java/com/beat/domain/booking/application/MemberBookingService.java index 2d6d9ccb..fbe57b3e 100644 --- a/src/main/java/com/beat/domain/booking/application/MemberBookingService.java +++ b/src/main/java/com/beat/domain/booking/application/MemberBookingService.java @@ -16,9 +16,11 @@ import com.beat.global.common.exception.BadRequestException; import com.beat.global.common.exception.NotFoundException; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class MemberBookingService { @@ -38,7 +40,7 @@ public MemberBookingResponse createMemberBooking(Long memberId, MemberBookingReq throw new BadRequestException(ScheduleErrorCode.INSUFFICIENT_TICKETS); } - schedule.setSoldTicketCount(schedule.getSoldTicketCount() + memberBookingRequest.purchaseTicketCount()); + updateSoldTicketCountAndIsBooking(schedule, memberBookingRequest.purchaseTicketCount()); Member member = memberRepository.findById(memberId).orElseThrow( () -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); @@ -50,7 +52,7 @@ public MemberBookingResponse createMemberBooking(Long memberId, MemberBookingReq memberBookingRequest.purchaseTicketCount(), memberBookingRequest.bookerName(), memberBookingRequest.bookerPhoneNumber(), - memberBookingRequest.isPaymentCompleted(), + memberBookingRequest.bookingStatus(), null, null, schedule, @@ -59,6 +61,8 @@ public MemberBookingResponse createMemberBooking(Long memberId, MemberBookingReq bookingRepository.save(booking); scheduleRepository.save(schedule); + log.info("Member Booking created: {}", booking); + return MemberBookingResponse.of( booking.getId(), schedule.getId(), @@ -67,11 +71,19 @@ public MemberBookingResponse createMemberBooking(Long memberId, MemberBookingReq schedule.getScheduleNumber(), booking.getBookerName(), booking.getBookerPhoneNumber(), - booking.isPaymentCompleted(), + booking.getBookingStatus(), schedule.getPerformance().getBankName(), schedule.getPerformance().getAccountNumber(), - memberBookingRequest.totalPaymentAmount(), + memberBookingRequest.totalPaymentAmount(), // 비회원 예매처럼 int totalPaymentAmount = ticketPrice * guestBookingRequest.purchaseTicketCount();로 계산해서 반영하기 + 요청한 총 가격 == 티켓 가격 * 수 같은지 검증하는 로직 추가하기 booking.getCreatedAt() ); } + + private void updateSoldTicketCountAndIsBooking(Schedule schedule, int purchaseTicketCount) { + schedule.setSoldTicketCount(schedule.getSoldTicketCount() + purchaseTicketCount); + + if (schedule.getTotalTicketCount() == schedule.getSoldTicketCount()) { + schedule.updateIsBooking(false); + } + } } \ No newline at end of file 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 4a5a4e58..cd0b9e4a 100644 --- a/src/main/java/com/beat/domain/booking/application/TicketService.java +++ b/src/main/java/com/beat/domain/booking/application/TicketService.java @@ -1,8 +1,25 @@ package com.beat.domain.booking.application; -import com.beat.domain.booking.application.dto.*; +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import net.nurigo.java_sdk.exceptions.CoolsmsException; + +import com.beat.domain.booking.application.dto.TicketCancelRequest; +import com.beat.domain.booking.application.dto.TicketDetail; +import com.beat.domain.booking.application.dto.TicketRetrieveResponse; +import com.beat.domain.booking.application.dto.TicketUpdateDetail; +import com.beat.domain.booking.application.dto.TicketUpdateRequest; import com.beat.domain.booking.dao.TicketRepository; import com.beat.domain.booking.domain.Booking; +import com.beat.domain.booking.domain.BookingStatus; +import com.beat.domain.booking.exception.BookingErrorCode; +import com.beat.domain.booking.exception.TicketErrorCode; import com.beat.domain.member.dao.MemberRepository; import com.beat.domain.member.domain.Member; import com.beat.domain.member.exception.MemberErrorCode; @@ -12,129 +29,145 @@ 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.exception.BookingErrorCode; -import com.beat.global.common.exception.ForbiddenException; -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 net.nurigo.java_sdk.exceptions.CoolsmsException; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.ForbiddenException; +import com.beat.global.common.exception.NotFoundException; -import java.util.List; -import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class TicketService { - private final TicketRepository ticketRepository; - private final PerformanceRepository performanceRepository; - private final MemberRepository memberRepository; - private final UserRepository userRepository; - private final ScheduleRepository scheduleRepository; - private final CoolSmsService coolSmsService; - - 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(PerformanceErrorCode.PERFORMANCE_NOT_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); - } - - 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)); - - boolean wasPaymentCompleted = booking.isPaymentCompleted(); - booking.setIsPaymentCompleted(detail.isPaymentCompleted()); - ticketRepository.save(booking); - - if (!wasPaymentCompleted && detail.isPaymentCompleted()) { - String message = String.format("%s님, BEAT에서의 %s의 예매가 확정되었습니다.", detail.bookerName(), request.performanceTitle()); - try { - coolSmsService.sendSms(detail.bookerPhoneNumber(), message); - } catch (CoolsmsException e) { - e.printStackTrace(); - } - } - } - } - - @Transactional - public void deleteTickets(Long memberId, TicketDeleteRequest ticketDeleteRequest) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); - - Long userId = member.getUser().getId(); - - Performance performance = performanceRepository.findById(ticketDeleteRequest.performanceId()) - .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_PERFORMANCE_FOUND)); - - if (!performance.getUsers().getId().equals(userId)) { - throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); - } - - for (Long bookingId : ticketDeleteRequest.bookingList()) { - Booking booking = ticketRepository.findById(bookingId) - .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); - - ticketRepository.delete(booking); - - Schedule schedule = booking.getSchedule(); - - ticketRepository.delete(booking); - - schedule.decreaseSoldTicketCount(booking.getPurchaseTicketCount()); - scheduleRepository.save(schedule); - } - } + private final TicketRepository ticketRepository; + private final PerformanceRepository performanceRepository; + private final MemberRepository memberRepository; + private final UserRepository userRepository; + private final ScheduleRepository scheduleRepository; + private final CoolSmsService coolSmsService; + + public TicketRetrieveResponse getTickets(Long memberId, Long performanceId, ScheduleNumber scheduleNumber, + BookingStatus bookingStatus) { + 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(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + + List bookings = findBookings(performanceId, scheduleNumber, bookingStatus); + + List bookingList = bookings.stream() + .map(booking -> TicketDetail.of( + booking.getId(), + booking.getBookerName(), + booking.getBookerPhoneNumber(), + booking.getSchedule().getId(), + booking.getPurchaseTicketCount(), + booking.getCreatedAt(), + booking.getBookingStatus(), + booking.getSchedule().getScheduleNumber().name() + )) + .collect(Collectors.toList()); + + return TicketRetrieveResponse.of( + performance.getPerformanceTitle(), + performance.getTotalScheduleCount(), + bookingList + ); + } + + private List findBookings(Long performanceId, ScheduleNumber scheduleNumber, BookingStatus bookingStatus) { + if (scheduleNumber != null && bookingStatus != null) { + return ticketRepository.findBySchedulePerformanceIdAndScheduleScheduleNumberAndBookingStatus(performanceId, + scheduleNumber, bookingStatus); + } else if (scheduleNumber != null) { + return ticketRepository.findBySchedulePerformanceIdAndScheduleScheduleNumber(performanceId, scheduleNumber); + } else if (bookingStatus != null) { + return ticketRepository.findBySchedulePerformanceIdAndBookingStatus(performanceId, bookingStatus); + } else { + return ticketRepository.findBySchedulePerformanceId(performanceId); + } + } + + @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)); + + if (booking.getBookingStatus() == BookingStatus.BOOKING_CONFIRMED + && detail.bookingStatus() != BookingStatus.BOOKING_CONFIRMED) { + throw new BadRequestException(TicketErrorCode.PAYMENT_COMPLETED_TICKET_UPDATE_NOT_ALLOWED); + } + + if (booking.getBookingStatus() == BookingStatus.CHECKING_PAYMENT + && detail.bookingStatus() == BookingStatus.BOOKING_CONFIRMED) { + booking.setBookingStatus(BookingStatus.BOOKING_CONFIRMED); + ticketRepository.save(booking); + + String message = String.format("[BEAT] %s님 %s 예매 확정되었습니다.", detail.bookerName(), + request.performanceTitle()); + try { + coolSmsService.sendSms(detail.bookerPhoneNumber(), message); + } catch (CoolsmsException e) { + e.printStackTrace(); + } + } + } + } + + @Transactional + public void cancelTickets(Long memberId, TicketCancelRequest ticketCancelRequest) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Long userId = member.getUser().getId(); + + Performance performance = performanceRepository.findById(ticketCancelRequest.performanceId()) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_PERFORMANCE_FOUND)); + + if (!performance.getUsers().getId().equals(userId)) { + throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); + } + + for (Long bookingId : ticketCancelRequest.bookingList()) { + Booking booking = ticketRepository.findById(bookingId) + .orElseThrow(() -> new NotFoundException(BookingErrorCode.NO_BOOKING_FOUND)); + + booking.setBookingStatus(BookingStatus.BOOKING_CANCELLED); + ticketRepository.save(booking); + + Schedule schedule = booking.getSchedule(); + schedule.decreaseSoldTicketCount(booking.getPurchaseTicketCount()); + + if (!schedule.isBooking()) { + schedule.updateIsBooking(true); + scheduleRepository.save(schedule); + } + } + } + + @Scheduled(cron = "0 0 4 * * ?") + @Transactional + public void deleteOldCancelledBookings() { + LocalDateTime oneYearAgo = LocalDateTime.now().minusYears(1); + List oldCancelledBookings = ticketRepository.findByBookingStatusAndCancellationDateBefore( + BookingStatus.BOOKING_CANCELLED, oneYearAgo); + ticketRepository.deleteAll(oldCancelledBookings); + } } 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 index 3ea0647d..891ccab3 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRequest.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.schedule.domain.ScheduleNumber; public record GuestBookingRequest( @@ -11,9 +12,9 @@ public record GuestBookingRequest( String birthDate, String password, int totalPaymentAmount, - boolean isPaymentCompleted + BookingStatus bookingStatus ) { - public static GuestBookingRequest of(Long scheduleId, int purchaseTicketCount, ScheduleNumber 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); + public static GuestBookingRequest of(Long scheduleId, int purchaseTicketCount, ScheduleNumber scheduleNumber, String bookerName, String bookerPhoneNumber, String birthDate, String password, int totalPaymentAmount, BookingStatus bookingStatus) { + return new GuestBookingRequest(scheduleId, purchaseTicketCount, scheduleNumber, bookerName, bookerPhoneNumber, birthDate, password, totalPaymentAmount, bookingStatus); } } \ 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 index ecf16df1..c9694cd5 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingResponse.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.performance.domain.BankName; import com.beat.domain.schedule.domain.ScheduleNumber; @@ -13,7 +14,7 @@ public record GuestBookingResponse( ScheduleNumber scheduleNumber, String bookerName, String bookerPhoneNumber, - boolean isPaymentCompleted, + BookingStatus bookingStatus, BankName bankName, String accountNumber, int totalPaymentAmount, @@ -27,7 +28,7 @@ public static GuestBookingResponse of( ScheduleNumber scheduleNumber, String bookerName, String bookerPhoneNumber, - boolean isPaymentCompleted, + BookingStatus bookingStatus, BankName bankName, String accountNumber, int totalPaymentAmount, @@ -40,7 +41,7 @@ public static GuestBookingResponse of( scheduleNumber, bookerName, bookerPhoneNumber, - isPaymentCompleted, + bookingStatus, bankName, accountNumber, totalPaymentAmount, diff --git a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java index 9a490638..65903380 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/GuestBookingRetrieveResponse.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.performance.domain.BankName; import com.beat.domain.schedule.domain.ScheduleNumber; @@ -20,7 +21,7 @@ public record GuestBookingRetrieveResponse( String accountNumber, String accountHolder, int dueDate, - boolean isPaymentCompleted, + BookingStatus bookingStatus, LocalDateTime createdAt, String posterImage, int totalPaymentAmount @@ -40,7 +41,7 @@ public static GuestBookingRetrieveResponse of( String accountNumber, String accountHolder, int dueDate, - boolean isPaymentCompleted, + BookingStatus bookingStatus, LocalDateTime createdAt, String posterImage, int totalPaymentAmount @@ -60,7 +61,7 @@ public static GuestBookingRetrieveResponse of( accountNumber, accountHolder, dueDate, - isPaymentCompleted, + bookingStatus, createdAt, posterImage, totalPaymentAmount 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 index d0a84a7d..608acfdd 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRequest.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.schedule.domain.ScheduleNumber; public record MemberBookingRequest( @@ -8,6 +9,6 @@ public record MemberBookingRequest( int purchaseTicketCount, String bookerName, String bookerPhoneNumber, - boolean isPaymentCompleted, + BookingStatus bookingStatus, int 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 index dce4f10d..9e3342fe 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingResponse.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.performance.domain.BankName; import com.beat.domain.schedule.domain.ScheduleNumber; @@ -13,7 +14,7 @@ public record MemberBookingResponse( ScheduleNumber scheduleNumber, String bookerName, String bookerPhoneNumber, - boolean isPaymentCompleted, + BookingStatus bookingStatus, BankName bankName, String accountNumber, int totalPaymentAmount, @@ -27,7 +28,7 @@ public static MemberBookingResponse of( ScheduleNumber scheduleNumber, String bookerName, String bookerPhoneNumber, - boolean isPaymentCompleted, + BookingStatus bookingStatus, BankName bankName, String accountNumber, int totalPaymentAmount, @@ -41,7 +42,7 @@ public static MemberBookingResponse of( scheduleNumber, bookerName, bookerPhoneNumber, - isPaymentCompleted, + bookingStatus, bankName, accountNumber, totalPaymentAmount, 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 index 134767bc..cfc82200 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java +++ b/src/main/java/com/beat/domain/booking/application/dto/MemberBookingRetrieveResponse.java @@ -1,5 +1,6 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.performance.domain.BankName; import com.beat.domain.schedule.domain.ScheduleNumber; @@ -21,7 +22,7 @@ public record MemberBookingRetrieveResponse( String accountNumber, String accountHolder, int dueDate, - boolean isPaymentCompleted, + BookingStatus bookingStatus, LocalDateTime createdAt, String posterImage, int totalPaymentAmount @@ -42,7 +43,7 @@ public static MemberBookingRetrieveResponse of( String accountNumber, String accountHolder, int dueDate, - boolean isPaymentCompleted, + BookingStatus bookingStatus, LocalDateTime createdAt, String posterImage, int totalPaymentAmount @@ -63,7 +64,7 @@ public static MemberBookingRetrieveResponse of( accountNumber, accountHolder, dueDate, - isPaymentCompleted, + bookingStatus, createdAt, posterImage, totalPaymentAmount diff --git a/src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java b/src/main/java/com/beat/domain/booking/application/dto/TicketCancelRequest.java similarity index 51% rename from src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java rename to src/main/java/com/beat/domain/booking/application/dto/TicketCancelRequest.java index 57c2e756..65341f8c 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/TicketDeleteRequest.java +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketCancelRequest.java @@ -2,11 +2,11 @@ import java.util.List; -public record TicketDeleteRequest( +public record TicketCancelRequest( Long performanceId, List bookingList ) { - public static TicketDeleteRequest of(Long performanceId, List bookingList) { - return new TicketDeleteRequest(performanceId, bookingList); + public static TicketCancelRequest of(Long performanceId, List bookingList) { + return new TicketCancelRequest(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 index 013eb32c..f72dee2f 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketDetail.java @@ -1,5 +1,7 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; + import java.time.LocalDateTime; public record TicketDetail( @@ -9,7 +11,7 @@ public record TicketDetail( Long scheduleId, int purchaseTicketCount, LocalDateTime createdAt, - boolean isPaymentCompleted, + BookingStatus bookingStatus, String scheduleNumber ) { public static TicketDetail of( @@ -19,8 +21,8 @@ public static TicketDetail of( Long scheduleId, int purchaseTicketCount, LocalDateTime createdAt, - boolean isPaymentCompleted, + BookingStatus bookingStatus, String scheduleNumber) { - return new TicketDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, isPaymentCompleted, scheduleNumber); + return new TicketDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, bookingStatus, scheduleNumber); } } 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 index 38e868df..79c2fd29 100644 --- a/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java +++ b/src/main/java/com/beat/domain/booking/application/dto/TicketUpdateDetail.java @@ -1,5 +1,7 @@ package com.beat.domain.booking.application.dto; +import com.beat.domain.booking.domain.BookingStatus; + import java.time.LocalDateTime; public record TicketUpdateDetail( @@ -9,7 +11,7 @@ public record TicketUpdateDetail( Long scheduleId, int purchaseTicketCount, LocalDateTime createdAt, - boolean isPaymentCompleted, + BookingStatus bookingStatus, String scheduleNumber ) { public static TicketUpdateDetail of( @@ -19,8 +21,8 @@ public static TicketUpdateDetail of( Long scheduleId, int purchaseTicketCount, LocalDateTime createdAt, - boolean isPaymentCompleted, + BookingStatus bookingStatus, String scheduleNumber) { - return new TicketUpdateDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, isPaymentCompleted, scheduleNumber); + return new TicketUpdateDetail(bookingId, bookerName, bookerPhoneNumber, scheduleId, purchaseTicketCount, createdAt, bookingStatus, scheduleNumber); } } 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 111be517..cd12d617 100644 --- a/src/main/java/com/beat/domain/booking/dao/BookingRepository.java +++ b/src/main/java/com/beat/domain/booking/dao/BookingRepository.java @@ -1,37 +1,38 @@ package com.beat.domain.booking.dao; -import com.beat.domain.booking.domain.Booking; +import java.util.List; +import java.util.Optional; + import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; -import java.util.List; -import java.util.Optional; +import com.beat.domain.booking.domain.Booking; public interface BookingRepository extends JpaRepository { - @Query("SELECT b FROM Booking b " + - "JOIN b.schedule s " + - "JOIN s.performance p " + - "WHERE b.bookerName = :bookerName " + - "AND b.bookerPhoneNumber = :bookerPhoneNumber " + - "AND b.password = :password " + - "AND b.birthDate = :birthDate") - Optional> findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthDate( - @Param("bookerName") String bookerName, - @Param("bookerPhoneNumber") String bookerPhoneNumber, - @Param("password") String password, - @Param("birthDate") String birthDate - ); + @Query("SELECT b FROM Booking b " + + "JOIN b.schedule s " + + "JOIN s.performance p " + + "WHERE b.bookerName = :bookerName " + + "AND b.bookerPhoneNumber = :bookerPhoneNumber " + + "AND b.password = :password " + + "AND b.birthDate = :birthDate") + Optional> findByBookerNameAndBookerPhoneNumberAndPasswordAndBirthDate( + @Param("bookerName") String bookerName, + @Param("bookerPhoneNumber") String bookerPhoneNumber, + @Param("password") String password, + @Param("birthDate") String birthDate + ); - Optional findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPassword( - String bookerName, - String bookerPhoneNumber, - String birthDate, - String password - ); + Optional findFirstByBookerNameAndBookerPhoneNumberAndBirthDateAndPassword( + String bookerName, + String bookerPhoneNumber, + String birthDate, + String password + ); - List findByUsersId(Long userId); + List findByUsersId(Long userId); - @Query("SELECT COUNT(b) > 0 FROM Booking b WHERE b.schedule.id IN :scheduleIds") - boolean existsByScheduleIdIn(@Param("scheduleIds") List scheduleIds); + @Query("SELECT COUNT(b) > 0 FROM Booking b WHERE b.schedule.id IN :scheduleIds AND b.bookingStatus != 'BOOKING_CANCELLED'") + boolean existsByScheduleIdIn(@Param("scheduleIds") List scheduleIds); } \ No newline at end of file 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 aad02e7e..870884e1 100644 --- a/src/main/java/com/beat/domain/booking/dao/TicketRepository.java +++ b/src/main/java/com/beat/domain/booking/dao/TicketRepository.java @@ -1,9 +1,11 @@ package com.beat.domain.booking.dao; import com.beat.domain.booking.domain.Booking; +import com.beat.domain.booking.domain.BookingStatus; import com.beat.domain.schedule.domain.ScheduleNumber; import org.springframework.data.jpa.repository.JpaRepository; +import java.time.LocalDateTime; import java.util.List; public interface TicketRepository extends JpaRepository { @@ -11,7 +13,9 @@ public interface TicketRepository extends JpaRepository { List findBySchedulePerformanceIdAndScheduleScheduleNumber(Long performanceId, ScheduleNumber scheduleNumber); - List findBySchedulePerformanceIdAndIsPaymentCompleted(Long performanceId, boolean isPaymentCompleted); + List findBySchedulePerformanceIdAndBookingStatus(Long performanceId, BookingStatus bookingStatus); - List findBySchedulePerformanceIdAndScheduleScheduleNumberAndIsPaymentCompleted(Long performanceId, ScheduleNumber scheduleNumber, boolean isPaymentCompleted); + List findBySchedulePerformanceIdAndScheduleScheduleNumberAndBookingStatus(Long performanceId, ScheduleNumber scheduleNumber, BookingStatus bookingStatus); + + List findByBookingStatusAndCancellationDateBefore(BookingStatus bookingStatus, LocalDateTime cancellationDate); } 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 39fba2fe..0b3ebfcf 100644 --- a/src/main/java/com/beat/domain/booking/domain/Booking.java +++ b/src/main/java/com/beat/domain/booking/domain/Booking.java @@ -4,6 +4,8 @@ import com.beat.domain.user.domain.Users; 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; @@ -38,11 +40,15 @@ public class Booking { private String bookerPhoneNumber; @Column(nullable = false) - private boolean isPaymentCompleted = false; + @Enumerated(EnumType.STRING) + private BookingStatus bookingStatus = BookingStatus.CHECKING_PAYMENT; @Column(nullable = false) private LocalDateTime createdAt = LocalDateTime.now(); + @Column(nullable = true) + private LocalDateTime cancellationDate; + @Column(nullable = true) private String birthDate; @@ -60,23 +66,23 @@ public class Booking { private Users users; @Builder - public Booking(int purchaseTicketCount, String bookerName, String bookerPhoneNumber, boolean isPaymentCompleted, String birthDate, String password, Schedule schedule, Users users) { + public Booking(int purchaseTicketCount, String bookerName, String bookerPhoneNumber, BookingStatus bookingStatus, String birthDate, String password, Schedule schedule, Users users) { this.purchaseTicketCount = purchaseTicketCount; this.bookerName = bookerName; this.bookerPhoneNumber = bookerPhoneNumber; - this.isPaymentCompleted = isPaymentCompleted; + this.bookingStatus = bookingStatus; 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) { + public static Booking create(int purchaseTicketCount, String bookerName, String bookerPhoneNumber, BookingStatus bookingStatus, String birthDate, String password, Schedule schedule, Users users) { return Booking.builder() .purchaseTicketCount(purchaseTicketCount) .bookerName(bookerName) .bookerPhoneNumber(bookerPhoneNumber) - .isPaymentCompleted(isPaymentCompleted) + .bookingStatus(bookingStatus) .birthDate(birthDate) .password(password) .schedule(schedule) @@ -84,8 +90,10 @@ public static Booking create(int purchaseTicketCount, String bookerName, String .build(); } - public void setIsPaymentCompleted(boolean isPaymentCompleted) { - this.isPaymentCompleted = isPaymentCompleted; + public void setBookingStatus(BookingStatus bookingStatus) { + this.bookingStatus = bookingStatus; + if (bookingStatus == BookingStatus.BOOKING_CANCELLED) { + this.cancellationDate = LocalDateTime.now(); + } } - } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/booking/domain/BookingStatus.java b/src/main/java/com/beat/domain/booking/domain/BookingStatus.java new file mode 100644 index 00000000..74d5a757 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/domain/BookingStatus.java @@ -0,0 +1,14 @@ +package com.beat.domain.booking.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum BookingStatus { + CHECKING_PAYMENT("입금확인중"), + BOOKING_CONFIRMED("예매 확정"), + BOOKING_CANCELLED("예매 취소"); + + private final String displayname; +} 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 e77708ff..a24a08ce 100644 --- a/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java +++ b/src/main/java/com/beat/domain/booking/exception/BookingSuccessCode.java @@ -13,7 +13,7 @@ public enum BookingSuccessCode implements BaseSuccessCode { GUEST_BOOKING_RETRIEVE_SUCCESS(200, "비회원 예매 조회가 성공적으로 완료되었습니다."), TICKET_RETRIEVE_SUCCESS(200, "예매자 목록 조회가 성공적으로 완료되었습니다."), TICKET_UPDATE_SUCCESS(200, "예매자 입금여부 수정이 성공적으로 완료되었습니다."), - TICKET_DELETE_SUCCESS(200, "예매자 삭제 요청이 성공했습니다.") + TICKET_CANCEL_SUCCESS(200, "예매취소 요청이 성공했습니다.") ; private final int status; diff --git a/src/main/java/com/beat/domain/booking/exception/TicketErrorCode.java b/src/main/java/com/beat/domain/booking/exception/TicketErrorCode.java new file mode 100644 index 00000000..b3376117 --- /dev/null +++ b/src/main/java/com/beat/domain/booking/exception/TicketErrorCode.java @@ -0,0 +1,14 @@ +package com.beat.domain.booking.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum TicketErrorCode implements BaseErrorCode { + PAYMENT_COMPLETED_TICKET_UPDATE_NOT_ALLOWED(400, "이미 결제가 완료된 티켓의 상태는 변경할 수 없습니다."); + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/cast/dao/CastRepository.java b/src/main/java/com/beat/domain/cast/dao/CastRepository.java index 37eb520b..98e6214b 100644 --- a/src/main/java/com/beat/domain/cast/dao/CastRepository.java +++ b/src/main/java/com/beat/domain/cast/dao/CastRepository.java @@ -2,6 +2,7 @@ import com.beat.domain.cast.domain.Cast; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; @@ -9,4 +10,7 @@ public interface CastRepository extends JpaRepository { List findByPerformanceId(Long performanceId); List findAllByPerformanceId(Long performanceId); + + @Query("SELECT c.id FROM Cast c WHERE c.performance.id = :performanceId") + List findIdsByPerformanceId(Long performanceId); } diff --git a/src/main/java/com/beat/domain/cast/exception/CastErrorCode.java b/src/main/java/com/beat/domain/cast/exception/CastErrorCode.java index 9658d8ab..15189c27 100644 --- a/src/main/java/com/beat/domain/cast/exception/CastErrorCode.java +++ b/src/main/java/com/beat/domain/cast/exception/CastErrorCode.java @@ -7,6 +7,7 @@ @Getter @RequiredArgsConstructor public enum CastErrorCode implements BaseErrorCode { + CAST_NOT_BELONG_TO_PERFORMANCE(403,"해당 등장인물은 해당 공연에 속해 있지 않습니다."), CAST_NOT_FOUND(404, "등장인물이 존재하지 않습니다.") ; 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 08c06b30..f246a74d 100644 --- a/src/main/java/com/beat/domain/member/api/MemberController.java +++ b/src/main/java/com/beat/domain/member/api/MemberController.java @@ -1,6 +1,8 @@ package com.beat.domain.member.api; +import com.beat.domain.member.application.AuthenticationService; import com.beat.domain.member.application.MemberService; +import com.beat.domain.member.application.SocialLoginService; import com.beat.domain.member.dto.*; import com.beat.domain.member.exception.MemberSuccessCode; import com.beat.global.auth.client.dto.MemberLoginRequest; @@ -20,8 +22,10 @@ @RequestMapping("/api/users") @RequiredArgsConstructor public class MemberController { - private final MemberService memberService; private final TokenService tokenService; + private final AuthenticationService authenticationService; + private final SocialLoginService socialLoginService; + private final static int COOKIE_MAX_AGE = 7 * 24 * 60 * 60; private final static String REFRESH_TOKEN = "refreshToken"; @@ -32,7 +36,7 @@ public ResponseEntity> signUp( @RequestBody final MemberLoginRequest loginRequest, HttpServletResponse response ) { - LoginSuccessResponse loginSuccessResponse = memberService.create(authorizationCode, loginRequest); + LoginSuccessResponse loginSuccessResponse = socialLoginService.handleSocialLogin(authorizationCode, loginRequest); ResponseCookie cookie = ResponseCookie.from(REFRESH_TOKEN, loginSuccessResponse.refreshToken()) .maxAge(COOKIE_MAX_AGE) .path("/") @@ -42,7 +46,7 @@ public ResponseEntity> signUp( .build(); response.setHeader("Set-Cookie", cookie.toString()); return ResponseEntity.status(HttpStatus.OK) - .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, LoginSuccessResponse.of(loginSuccessResponse.accessToken(), null, loginSuccessResponse.nickname()))); + .body(SuccessResponse.of(MemberSuccessCode.SIGN_UP_SUCCESS, LoginSuccessResponse.of(loginSuccessResponse.accessToken(), null, loginSuccessResponse.nickname(), loginSuccessResponse.role()))); } @Operation(summary = "access token 재발급 API", description = "refresh token으로 access token을 재발급하는 GET API입니다.") @@ -50,7 +54,7 @@ public ResponseEntity> signUp( public ResponseEntity> refreshToken( @RequestParam final String refreshToken ) { - AccessTokenGetSuccess accessTokenGetSuccess = memberService.refreshToken(refreshToken); + AccessTokenGetSuccess accessTokenGetSuccess = authenticationService.generateAccessTokenFromRefreshToken(refreshToken); return ResponseEntity.status(HttpStatus.OK) .body(SuccessResponse.of(MemberSuccessCode.ISSUE_REFRESH_TOKEN_SUCCESS, accessTokenGetSuccess)); } diff --git a/src/main/java/com/beat/domain/member/application/AuthenticationService.java b/src/main/java/com/beat/domain/member/application/AuthenticationService.java new file mode 100644 index 00000000..49e844c7 --- /dev/null +++ b/src/main/java/com/beat/domain/member/application/AuthenticationService.java @@ -0,0 +1,134 @@ +package com.beat.domain.member.application; + +import com.beat.domain.member.dto.AccessTokenGetSuccess; +import com.beat.domain.member.dto.LoginSuccessResponse; +import com.beat.domain.user.domain.Role; +import com.beat.domain.user.domain.Users; +import com.beat.global.auth.client.dto.MemberInfoResponse; +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.jwt.provider.JwtValidationType; +import com.beat.global.auth.security.AdminAuthentication; +import com.beat.global.auth.security.MemberAuthentication; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.BeatException; +import com.beat.global.common.exception.UnauthorizedException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Collection; +import java.util.List; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AuthenticationService { + + private final JwtTokenProvider jwtTokenProvider; + private final TokenService tokenService; + + /** + * 사용자의 로그인 성공 시 Access Token과 Refresh Token을 생성하고, + * 로그인 성공 응답 객체(LoginSuccessResponse)를 반환하는 메서드. + * + * @param memberId 회원의 고유 ID + * @param user 로그인한 사용자의 정보가 포함된 Users 객체 + * @param memberInfoResponse 로그인 시 외부로부터 전달된 회원 정보 + * @return 로그인 성공 응답(LoginSuccessResponse) + */ + public LoginSuccessResponse generateLoginSuccessResponse(final Long memberId, final Users user, final MemberInfoResponse memberInfoResponse) { + String nickname = memberInfoResponse.nickname(); + Role role = user.getRole(); + Collection authorities = List.of(role.toGrantedAuthority()); + + log.info("Starting login success response generation for memberId: {}, nickname: {}, role: {}", memberId, nickname, role.getRoleName()); + + UsernamePasswordAuthenticationToken authenticationToken = createAuthenticationToken(memberId, role, authorities); + String refreshToken = issueAndSaveRefreshToken(memberId, authenticationToken); + String accessToken = jwtTokenProvider.issueAccessToken(authenticationToken); + + log.info("Login success for authorities: {}, accessToken: {}, refreshToken: {}", authorities, accessToken, refreshToken); + + return LoginSuccessResponse.of(accessToken, refreshToken, nickname, role.getRoleName()); + } + + /** + * Refresh Token을 사용하여 새로운 Access Token을 생성하는 메서드. + * + * Refresh Token에서 사용자 ID와 Role 정보를 추출한 후, + * Role에 따라 Admin 또는 Member 권한으로 새로운 Access Token을 발급합니다. + * + * @param refreshToken 사용자의 Refresh Token + * @return 새로운 Access Token 정보가 포함된 AccessTokenGetSuccess 객체 + */ + @Transactional + public AccessTokenGetSuccess generateAccessTokenFromRefreshToken(final String refreshToken) { + log.info("Validation result for refresh token: {}", jwtTokenProvider.validateToken(refreshToken)); + + JwtValidationType validationType = jwtTokenProvider.validateToken(refreshToken); + if (!validationType.equals(JwtValidationType.VALID_JWT)) { + log.warn("Invalid refresh token: {}", validationType); + throw switch (validationType) { + case EXPIRED_JWT_TOKEN -> new UnauthorizedException(TokenErrorCode.REFRESH_TOKEN_EXPIRED_ERROR); + case INVALID_JWT_TOKEN -> new BadRequestException(TokenErrorCode.INVALID_REFRESH_TOKEN_ERROR); + case INVALID_JWT_SIGNATURE -> new BadRequestException(TokenErrorCode.REFRESH_TOKEN_SIGNATURE_ERROR); + case UNSUPPORTED_JWT_TOKEN -> new BadRequestException(TokenErrorCode.UNSUPPORTED_REFRESH_TOKEN_ERROR); + case EMPTY_JWT -> new BadRequestException(TokenErrorCode.REFRESH_TOKEN_EMPTY_ERROR); + default -> new BeatException(TokenErrorCode.UNKNOWN_REFRESH_TOKEN_ERROR); + }; + } + + Long memberId = jwtTokenProvider.getMemberIdFromJwt(refreshToken); + + if (!memberId.equals(tokenService.findIdByRefreshToken(refreshToken))) { + log.error("MemberId mismatch: token does not match the stored refresh token"); + throw new BadRequestException(TokenErrorCode.REFRESH_TOKEN_MEMBER_ID_MISMATCH_ERROR); + } + + Role role = jwtTokenProvider.getRoleFromJwt(refreshToken); + Collection authorities = List.of(role.toGrantedAuthority()); + + UsernamePasswordAuthenticationToken authenticationToken = createAuthenticationToken(memberId, role, authorities); + log.info("Generated new access token for memberId: {}, role: {}, authorities: {}", + memberId, role.getRoleName(), authorities); + return AccessTokenGetSuccess.of(jwtTokenProvider.issueAccessToken(authenticationToken)); + } + + /** + * Refresh Token을 발급하고 저장하는 메서드. + * 발급된 Refresh Token을 TokenService에 저장 + * + * @param memberId 회원의 고유 ID + * @param authenticationToken 사용자 인증 정보 + * @return 발급된 Refresh Token + */ + private String issueAndSaveRefreshToken(Long memberId, UsernamePasswordAuthenticationToken authenticationToken) { + String refreshToken = jwtTokenProvider.issueRefreshToken(authenticationToken); + log.info("Issued new refresh token for memberId: {}", memberId); + tokenService.saveRefreshToken(memberId, refreshToken); + return refreshToken; + } + + /** + * 사용자 Role에 따라 적절한 Authentication 객체(Admin 또는 Member)를 생성하는 메서드. + * + * @param memberId 회원의 고유 ID + * @param role 사용자 Role (ADMIN 또는 MEMBER) + * @param authorities 사용자에게 부여된 권한 목록 + * @return 생성된 Admin 또는 Member Authentication 객체 + */ + private UsernamePasswordAuthenticationToken createAuthenticationToken(Long memberId, Role role, Collection authorities) { + if (role == Role.ADMIN) { + log.info("Creating AdminAuthentication for memberId: {}", memberId); + return new AdminAuthentication(memberId, null, authorities); + } else { + log.info("Creating MemberAuthentication for memberId: {}", memberId); + return new MemberAuthentication(memberId, null, authorities); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/application/MemberRegistrationService.java b/src/main/java/com/beat/domain/member/application/MemberRegistrationService.java new file mode 100644 index 00000000..5f19fc65 --- /dev/null +++ b/src/main/java/com/beat/domain/member/application/MemberRegistrationService.java @@ -0,0 +1,49 @@ +package com.beat.domain.member.application; + +import com.beat.domain.member.dao.MemberRepository; +import com.beat.domain.member.domain.Member; +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Role; +import com.beat.domain.user.domain.Users; +import com.beat.global.auth.client.dto.MemberInfoResponse; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class MemberRegistrationService { + + private final UserRepository userRepository; + private final MemberRepository memberRepository; + + @Transactional + public Long registerMemberWithUserInfo(final MemberInfoResponse memberInfoResponse) { + Users users = Users.createWithRole(Role.MEMBER); + + log.info("Granting MEMBER role to new user with role: {}", users.getRole()); + + users = userRepository.save(users); + userRepository.flush(); + + log.info("Registering new user with role: {}", users.getRole()); + + Member member = Member.create( + memberInfoResponse.nickname(), + memberInfoResponse.email(), + users, + memberInfoResponse.socialId(), + memberInfoResponse.socialType() + ); + + memberRepository.save(member); + + log.info("Member registered with memberId: {}, role: {}", member.getId(), users.getRole()); + + return member.getId(); + } +} \ No newline at end of file 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 d0c68a6f..5fdb3a4b 100644 --- a/src/main/java/com/beat/domain/member/application/MemberService.java +++ b/src/main/java/com/beat/domain/member/application/MemberService.java @@ -3,20 +3,15 @@ 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.member.port.in.MemberUseCase; 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; @@ -24,114 +19,35 @@ @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; - - @Transactional - 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.create(); - users = userRepository.save(users); - - 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; - } - - @Transactional - 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); - } - } +public class MemberService implements MemberUseCase { + private final UserRepository userRepository; + private final MemberRepository memberRepository; + + @Override + @Transactional(readOnly = true) + public Member findMemberByMemberId(Long memberId) { + return memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Override + @Transactional(readOnly = true) + public boolean checkMemberExistsBySocialIdAndSocialType(final Long socialId, final SocialType socialType) { + return memberRepository.findBySocialTypeAndSocialId(socialId, socialType).isPresent(); + } + + @Override + @Transactional(readOnly = true) + public Member findMemberBySocialIdAndSocialType(final Long socialId, final SocialType socialType) { + return memberRepository.findBySocialTypeAndSocialId(socialId, socialType) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + } + + @Transactional + public void deleteUser(final Long id) { + Users users = userRepository.findById(id) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + userRepository.delete(users); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/member/application/SocialLoginService.java b/src/main/java/com/beat/domain/member/application/SocialLoginService.java new file mode 100644 index 00000000..36b304c9 --- /dev/null +++ b/src/main/java/com/beat/domain/member/application/SocialLoginService.java @@ -0,0 +1,119 @@ +package com.beat.domain.member.application; + +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.domain.SocialType; +import com.beat.domain.member.dto.LoginSuccessResponse; +import com.beat.domain.member.exception.MemberErrorCode; +import com.beat.domain.member.port.in.MemberUseCase; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.port.in.UserUseCase; +import com.beat.global.auth.client.application.KakaoSocialService; +import com.beat.global.auth.client.application.SocialService; +import com.beat.global.auth.client.dto.MemberInfoResponse; +import com.beat.global.auth.client.dto.MemberLoginRequest; +import com.beat.global.common.exception.BadRequestException; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@RequiredArgsConstructor +@Service +public class SocialLoginService { + + private final MemberRegistrationService memberRegistrationService; + private final AuthenticationService authenticationService; + private final KakaoSocialService kakaoSocialService; + private final MemberUseCase memberUseCase; + + /** + * 소셜 로그인 또는 회원가입을 처리하는 메서드. + * 소셜 서비스에서 받은 authorizationCode와 로그인 요청 정보를 기반으로 + * 사용자 정보를 조회하고, 로그인 또는 회원가입 후 성공 응답을 반환. + * + * @param authorizationCode 소셜 인증 코드 + * @param loginRequest 로그인 요청 정보 + * @return 로그인 성공 응답(LoginSuccessResponse) + */ + @Transactional + public LoginSuccessResponse handleSocialLogin(final String authorizationCode, + final MemberLoginRequest loginRequest) { + MemberInfoResponse memberInfoResponse = findMemberInfoFromSocialService(authorizationCode, loginRequest); + return generateLoginResponseFromMemberInfo(memberInfoResponse); + } + + /** + * 소셜 서비스에서 사용자 정보를 조회하는 메서드. + * 소셜 타입에 따라 적절한 소셜 서비스를 사용하여 로그인 정보를 가져옴. + * + * @param authorizationCode 소셜 인증 코드 + * @param loginRequest 로그인 요청 정보 + * @return 소셜 서비스에서 가져온 사용자 정보(MemberInfoResponse) + */ + private MemberInfoResponse findMemberInfoFromSocialService(final String authorizationCode, + final MemberLoginRequest loginRequest) { + SocialService socialService = findSocialService(loginRequest.socialType()); + return socialService.login(authorizationCode, loginRequest); + } + + /** + * 소셜 타입에 맞는 SocialService를 반환하는 메서드. + * 소셜 로그인 타입이 KAKAO인지, GOOGLE인지 등에 따라 적절한 서비스를 반환. + * + * @param socialType 소셜 타입(KAKAO, GOOGLE 등) + * @return 적절한 SocialService 구현체 + */ + private SocialService findSocialService(SocialType socialType) { + return switch (socialType) { + case KAKAO -> kakaoSocialService; + // case GOOGLE -> googleSocialService; + default -> throw new BadRequestException(MemberErrorCode.SOCIAL_TYPE_BAD_REQUEST); + }; + } + + /** + * 사용자 정보를 기반으로 로그인 또는 회원가입을 처리한 후 로그인 성공 응답을 생성하는 메서드. + * 사용자가 존재하면 로그인 처리를, 존재하지 않으면 회원가입 후 로그인 처리를 수행. + * + * @param memberInfoResponse 소셜 서비스에서 가져온 사용자 정보 + * @return 로그인 성공 응답(LoginSuccessResponse) + */ + private LoginSuccessResponse generateLoginResponseFromMemberInfo(final MemberInfoResponse memberInfoResponse) { + log.info("Attempting to find or register member for socialId: {}, socialType: {}", + memberInfoResponse.socialId(), memberInfoResponse.socialType()); + + Long memberId = findOrRegisterMember(memberInfoResponse); + log.info("Found or registered member with memberId: {}", memberId); + + Member member = memberUseCase.findMemberByMemberId(memberId); + Users user = member.getUser(); + + log.info("User role before generating token: {}", user.getRole()); + + return authenticationService.generateLoginSuccessResponse(memberId, user, memberInfoResponse); + } + + /** + * 사용자 정보(Social ID와 Social Type)를 통해 기존 회원을 찾거나, + * 없으면 새로운 회원을 등록하는 메서드. + * + * @param memberInfoResponse 소셜 서비스에서 가져온 사용자 정보 + * @return 등록된 회원 또는 기존 회원의 ID + */ + private Long findOrRegisterMember(final MemberInfoResponse memberInfoResponse) { + boolean memberExists = memberUseCase.checkMemberExistsBySocialIdAndSocialType(memberInfoResponse.socialId(), + memberInfoResponse.socialType()); + + if (memberExists) { + Member existingMember = memberUseCase.findMemberBySocialIdAndSocialType(memberInfoResponse.socialId(), + memberInfoResponse.socialType()); + log.info("Existing member role: {}", existingMember.getUser().getRole()); + return existingMember.getId(); + } + + return memberRegistrationService.registerMemberWithUserInfo(memberInfoResponse); + } +} \ No newline at end of file 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 90951b30..02a67ac7 100644 --- a/src/main/java/com/beat/domain/member/domain/Member.java +++ b/src/main/java/com/beat/domain/member/domain/Member.java @@ -2,7 +2,17 @@ import com.beat.domain.BaseTimeEntity; import com.beat.domain.user.domain.Users; -import jakarta.persistence.*; +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; @@ -13,7 +23,7 @@ @Entity @Getter -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Member extends BaseTimeEntity { @Id @@ -29,7 +39,7 @@ public class Member extends BaseTimeEntity { @Column(nullable = true) private LocalDateTime deletedAt; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "user_id", nullable = true) @OnDelete(action = OnDeleteAction.CASCADE) private Users user; diff --git a/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java b/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java index 57b0576e..026bd3d1 100644 --- a/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java +++ b/src/main/java/com/beat/domain/member/dto/LoginSuccessResponse.java @@ -3,13 +3,15 @@ public record LoginSuccessResponse( String accessToken, String refreshToken, - String nickname + String nickname, + String role ) { public static LoginSuccessResponse of( final String accessToken, final String refreshToken, - final String nickname + final String nickname, + final String role ) { - return new LoginSuccessResponse(accessToken, refreshToken, nickname); + return new LoginSuccessResponse(accessToken, refreshToken, nickname, role); } } diff --git a/src/main/java/com/beat/domain/member/port/in/MemberUseCase.java b/src/main/java/com/beat/domain/member/port/in/MemberUseCase.java new file mode 100644 index 00000000..d6a194b1 --- /dev/null +++ b/src/main/java/com/beat/domain/member/port/in/MemberUseCase.java @@ -0,0 +1,14 @@ +package com.beat.domain.member.port.in; + +import com.beat.domain.member.domain.Member; +import com.beat.domain.member.domain.SocialType; + +public interface MemberUseCase { + Member findMemberByMemberId(Long memberId); + + boolean checkMemberExistsBySocialIdAndSocialType(Long socialId, SocialType socialType); + + Member findMemberBySocialIdAndSocialType(Long socialId, SocialType socialType); + + void deleteUser(Long id); +} diff --git a/src/main/java/com/beat/domain/performance/api/HomeApi.java b/src/main/java/com/beat/domain/performance/api/HomeApi.java new file mode 100644 index 00000000..0b6e9e58 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/api/HomeApi.java @@ -0,0 +1,33 @@ +package com.beat.domain.performance.api; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; + +import com.beat.domain.performance.application.dto.home.HomeFindAllResponse; +import com.beat.domain.performance.domain.Genre; +import com.beat.global.common.dto.ErrorResponse; +import com.beat.global.common.dto.SuccessResponse; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; + +@Tag(name = "Home", description = "홈 화면에서 공연 및 홍보목록 조회 API") +public interface HomeApi { + + @Operation(summary = "전체 공연 및 홍보 목록 조회", description = "홈 화면에서 전체 공연 목록 및 홍보 목록을 조회하는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "홈 화면 공연 목록 조회가 성공적으로 완료되었습니다.", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ) + } + ) + ResponseEntity> getHomePerformanceList( + @RequestParam(required = false) Genre genre); +} \ No newline at end of file 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 7a7a7159..8ebd44b2 100644 --- a/src/main/java/com/beat/domain/performance/api/HomeController.java +++ b/src/main/java/com/beat/domain/performance/api/HomeController.java @@ -1,13 +1,14 @@ package com.beat.domain.performance.api; -import com.beat.domain.performance.application.PerformanceService; -import com.beat.domain.performance.application.dto.home.HomeRequest; -import com.beat.domain.performance.application.dto.home.HomeResponse; +import com.beat.domain.performance.application.HomeService; +import com.beat.domain.performance.application.dto.home.HomeFindRequest; +import com.beat.domain.performance.application.dto.home.HomeFindAllResponse; 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; import org.springframework.web.bind.annotation.RequestMapping; @@ -17,19 +18,19 @@ @RestController @RequestMapping("/api/main") @RequiredArgsConstructor -public class HomeController { - - private final PerformanceService performanceService; - - @Operation(summary = "전체공연목록, 홍보목록 조회 API", description = "홈화면에서 전체공연목록, 홍보목록을 조회하는 GET API입니다.") - @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)); - } +public class HomeController implements HomeApi { + + private final HomeService homeService; + + @Override + @GetMapping + public ResponseEntity> getHomePerformanceList( + @RequestParam(required = false) Genre genre) { + + HomeFindRequest request = new HomeFindRequest(genre); + + HomeFindAllResponse response = homeService.findHomePerformanceList(request); + return ResponseEntity.ok( + SuccessResponse.of(PerformanceSuccessCode.HOME_PERFORMANCE_RETRIEVE_SUCCESS, response)); + } } \ No newline at end of file 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 adf3a9a4..6cc25c77 100644 --- a/src/main/java/com/beat/domain/performance/api/PerformanceController.java +++ b/src/main/java/com/beat/domain/performance/api/PerformanceController.java @@ -1,20 +1,22 @@ package com.beat.domain.performance.api; import com.beat.domain.performance.application.PerformanceManagementService; -import com.beat.domain.performance.application.PerformanceUpdateService; -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.PerformanceEditResponse; +import com.beat.domain.performance.application.PerformanceModifyService; +import com.beat.domain.performance.application.dto.bookingPerformanceDetail.BookingPerformanceDetailResponse; +import com.beat.domain.performance.application.dto.makerPerformance.MakerPerformanceResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailResponse; +import com.beat.domain.performance.application.dto.modify.PerformanceModifyDetailResponse; 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.update.PerformanceUpdateRequest; -import com.beat.domain.performance.application.dto.update.PerformanceUpdateResponse; +import com.beat.domain.performance.application.dto.modify.PerformanceModifyRequest; +import com.beat.domain.performance.application.dto.modify.PerformanceModifyResponse; 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 io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -34,7 +36,7 @@ public class PerformanceController { private final PerformanceService performanceService; private final PerformanceManagementService performanceManagementService; - private final PerformanceUpdateService performanceUpdateService; + private final PerformanceModifyService performanceModifyService; @Operation(summary = "공연 생성 API", description = "공연을 생성하는 POST API입니다.") @PostMapping @@ -47,21 +49,35 @@ public ResponseEntity> createPerformance( } @Operation(summary = "공연 정보 수정 API", description = "공연 정보를 수정하는 PUT API입니다.") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "공연 정보 수정 성공"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 - 회차 최대 개수 초과"), + @ApiResponse(responseCode = "400", description = "잘못된 요청 - 티켓 가격은 음수일 수 없습니다."), + @ApiResponse(responseCode = "400", description = "잘못된 요청 - 예매자가 존재하여 가격을 수정할 수 없습니다."), + @ApiResponse(responseCode = "403", description = "권한 없음 - 해당 공연의 소유자가 아닙니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 공연 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회원 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 회차 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 등장인물 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 스태프 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "404", description = "존재하지 않는 상세이미지 ID로 수정 요청을 보낼 수 없습니다."), + @ApiResponse(responseCode = "500", description = "서버 내부 오류") + }) @PutMapping - public ResponseEntity> updatePerformance( + public ResponseEntity> updatePerformance( @CurrentMember Long memberId, - @RequestBody PerformanceUpdateRequest performanceUpdateRequest) { - PerformanceUpdateResponse response = performanceUpdateService.updatePerformance(memberId, performanceUpdateRequest); + @RequestBody PerformanceModifyRequest performanceModifyRequest) { + PerformanceModifyResponse response = performanceModifyService.modifyPerformance(memberId, performanceModifyRequest); return ResponseEntity.status(HttpStatus.OK) .body(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_UPDATE_SUCCESS, response)); } @Operation(summary = "공연 수정 페이지 정보 조회 API", description = "공연 정보를 조회하는 GET API입니다.") @GetMapping("/{performanceId}") - public ResponseEntity> getPerformanceForEdit( + public ResponseEntity> getPerformanceForEdit( @CurrentMember Long memberId, @PathVariable Long performanceId) { - PerformanceEditResponse response = performanceService.getPerformanceEdit(memberId, performanceId); + PerformanceModifyDetailResponse response = performanceService.getPerformanceEdit(memberId, performanceId); return ResponseEntity.ok(SuccessResponse.of(PerformanceSuccessCode.PERFORMANCE_MODIFY_PAGE_SUCCESS, response)); } diff --git a/src/main/java/com/beat/domain/performance/application/HomeService.java b/src/main/java/com/beat/domain/performance/application/HomeService.java new file mode 100644 index 00000000..6ac6887c --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/HomeService.java @@ -0,0 +1,71 @@ +package com.beat.domain.performance.application; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.beat.domain.performance.application.dto.home.HomeFindAllResponse; +import com.beat.domain.performance.application.dto.home.HomeFindRequest; +import com.beat.domain.performance.application.dto.home.HomePerformanceDetail; +import com.beat.domain.performance.application.dto.home.HomePromotionDetail; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.Genre; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.promotion.domain.Promotion; +import com.beat.domain.promotion.port.in.PromotionUseCase; +import com.beat.domain.schedule.application.ScheduleService; + +import lombok.RequiredArgsConstructor; + +@Service +@RequiredArgsConstructor +public class HomeService { + + private final ScheduleService scheduleService; + private final PromotionUseCase promotionUseCase; + + private final PerformanceRepository performanceRepository; + + @Transactional(readOnly = true) + public HomeFindAllResponse findHomePerformanceList(HomeFindRequest homeFindRequest) { + + List performances = findPerformancesByGenre(homeFindRequest); + List promotions = findAllPromotionsSortedByCarouselNumber(); + + if (performances.isEmpty()) { + return HomeFindAllResponse.of(promotions, new ArrayList<>()); + } + + List sortedPerformances = performances.stream() + .map(performance -> { + int minDueDate = scheduleService.getMinDueDateForPerformance(performance.getId()); + return HomePerformanceDetail.of(performance, minDueDate); + }) + .sorted(Comparator.comparingInt(detail -> detail.dueDate() < 0 ? 1 : 0) + .thenComparingInt(detail -> Math.abs(detail.dueDate()))) + .toList(); + + return HomeFindAllResponse.of(promotions, sortedPerformances); + } + + private List findPerformancesByGenre(HomeFindRequest homeFindRequest) { + Genre genre = homeFindRequest.genre(); + + if (genre != null) { + return performanceRepository.findByGenre(genre); + } + + return performanceRepository.findAll(); + } + + private List findAllPromotionsSortedByCarouselNumber() { + return promotionUseCase.findAllPromotions() + .stream() + .sorted(Comparator.comparing(Promotion::getCarouselNumber, Comparator.comparingInt(Enum::ordinal))) + .map(HomePromotionDetail::from) + .toList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceManagementService.java b/src/main/java/com/beat/domain/performance/application/PerformanceManagementService.java index 0e3efa88..289a77ac 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceManagementService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceManagementService.java @@ -1,5 +1,14 @@ package com.beat.domain.performance.application; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.beat.domain.booking.dao.BookingRepository; import com.beat.domain.cast.dao.CastRepository; import com.beat.domain.cast.domain.Cast; @@ -7,181 +16,220 @@ import com.beat.domain.member.domain.Member; import com.beat.domain.member.exception.MemberErrorCode; import com.beat.domain.performance.application.dto.create.CastResponse; +import com.beat.domain.performance.application.dto.create.PerformanceImageResponse; 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.PerformanceImageRepository; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.domain.PerformanceImage; import com.beat.domain.performance.exception.PerformanceErrorCode; 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.domain.Users; +import com.beat.global.common.exception.BadRequestException; import com.beat.global.common.exception.ForbiddenException; import com.beat.global.common.exception.NotFoundException; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import com.beat.global.common.scheduler.application.JobSchedulerService; -import java.time.LocalDate; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; @Service @RequiredArgsConstructor public class PerformanceManagementService { - private final PerformanceRepository performanceRepository; - private final ScheduleRepository scheduleRepository; - private final CastRepository castRepository; - private final StaffRepository staffRepository; - private final BookingRepository bookingRepository; - private final MemberRepository memberRepository; - - @Transactional - public PerformanceResponse createPerformance(Long memberId, PerformanceRequest request) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); - - Users user = member.getUser(); - - 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); - } - - @Transactional - public void deletePerformance(Long memberId, Long performanceId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); - - Long userId = member.getUser().getId(); - - Performance performance = performanceRepository.findById(performanceId) - .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); - - if (!performance.getUsers().getId().equals(userId)) { - throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); - } - - List scheduleIds = scheduleRepository.findIdsByPerformanceId(performanceId); - - boolean hasBookings = bookingRepository.existsByScheduleIdIn(scheduleIds); - - if (hasBookings) { - throw new ForbiddenException(PerformanceErrorCode.PERFORMANCE_DELETE_FAILED); - } - - performanceRepository.delete(performance); - } + private final PerformanceRepository performanceRepository; + private final ScheduleRepository scheduleRepository; + private final CastRepository castRepository; + private final StaffRepository staffRepository; + private final BookingRepository bookingRepository; + private final MemberRepository memberRepository; + private final PerformanceImageRepository performanceImageRepository; + private final JobSchedulerService jobSchedulerService; + + @Transactional + public PerformanceResponse createPerformance(Long memberId, PerformanceRequest request) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Users user = member.getUser(); + + if (request.performanceDescription().length() > 500) { + throw new BadRequestException(PerformanceErrorCode.INVALID_PERFORMANCE_DESCRIPTION_LENGTH); + } + + if (request.performanceAttentionNote().length() > 500) { + throw new BadRequestException(PerformanceErrorCode.INVALID_ATTENTION_NOTE_LENGTH); + } + + 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 -> { + if (scheduleRequest.performanceDate().isBefore(LocalDateTime.now())) { + throw new BadRequestException(PerformanceErrorCode.PAST_SCHEDULE_NOT_ALLOWED); + } + return Schedule.create( + scheduleRequest.performanceDate(), + scheduleRequest.totalTicketCount(), + 0, + true, + scheduleRequest.scheduleNumber(), + performance + ); + }) + .collect(Collectors.toList()); + scheduleRepository.saveAll(schedules); + + schedules.forEach(jobSchedulerService::addScheduleIfNotExists); + + 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); + + List performanceImageList = request.performanceImageList().stream() + .map(performanceImageRequest -> PerformanceImage.create( + performanceImageRequest.performanceImage(), + performance + )) + .collect(Collectors.toList()); + performanceImageRepository.saveAll(performanceImageList); + + return mapToPerformanceResponse(performance, schedules, casts, staffs, performanceImageList); + } + + private PerformanceResponse mapToPerformanceResponse(Performance performance, List schedules, + List casts, List staffs, List performanceImages) { + 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()); + + List performanceImageResponses = performanceImages.stream() + .map(image -> PerformanceImageResponse.of( + image.getId(), + image.getPerformanceImage() + )) + .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, + performanceImageResponses + ); + } + + private int calculateDueDate(LocalDate performanceDate) { + return (int)ChronoUnit.DAYS.between(LocalDate.now(), performanceDate); + } + + @Transactional + public void deletePerformance(Long memberId, Long performanceId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Long userId = member.getUser().getId(); + + Performance performance = performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + + if (!performance.getUsers().getId().equals(userId)) { + throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); + } + + List scheduleIds = scheduleRepository.findIdsByPerformanceId(performanceId); + + boolean hasBookings = bookingRepository.existsByScheduleIdIn(scheduleIds); + + if (hasBookings) { + throw new ForbiddenException(PerformanceErrorCode.PERFORMANCE_DELETE_FAILED); + } + + // 모든 스케줄에 대해 등록된 TaskScheduler 작업을 취소 + List schedules = scheduleRepository.findByPerformanceId(performanceId); + for (Schedule schedule : schedules) { + jobSchedulerService.cancelScheduledTaskForPerformance(schedule); + } + + performanceRepository.delete(performance); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceModifyService.java b/src/main/java/com/beat/domain/performance/application/PerformanceModifyService.java new file mode 100644 index 00000000..48db2f41 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/PerformanceModifyService.java @@ -0,0 +1,603 @@ +package com.beat.domain.performance.application; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.cast.dao.CastRepository; +import com.beat.domain.cast.domain.Cast; +import com.beat.domain.cast.exception.CastErrorCode; +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.modify.PerformanceModifyRequest; +import com.beat.domain.performance.application.dto.modify.PerformanceModifyResponse; +import com.beat.domain.performance.application.dto.modify.cast.CastModifyRequest; +import com.beat.domain.performance.application.dto.modify.cast.CastModifyResponse; +import com.beat.domain.performance.application.dto.modify.performanceImage.PerformanceImageModifyRequest; +import com.beat.domain.performance.application.dto.modify.performanceImage.PerformanceImageModifyResponse; +import com.beat.domain.performance.application.dto.modify.schedule.ScheduleModifyRequest; +import com.beat.domain.performance.application.dto.modify.schedule.ScheduleModifyResponse; +import com.beat.domain.performance.application.dto.modify.staff.StaffModifyRequest; +import com.beat.domain.performance.application.dto.modify.staff.StaffModifyResponse; +import com.beat.domain.performance.dao.PerformanceImageRepository; +import com.beat.domain.performance.dao.PerformanceRepository; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.domain.PerformanceImage; +import com.beat.domain.performance.exception.PerformanceErrorCode; +import com.beat.domain.performance.exception.PerformanceImageErrorCode; +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.schedule.exception.ScheduleErrorCode; +import com.beat.domain.staff.dao.StaffRepository; +import com.beat.domain.staff.domain.Staff; +import com.beat.domain.staff.exception.StaffErrorCode; +import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.ForbiddenException; +import com.beat.global.common.exception.NotFoundException; +import com.beat.global.common.scheduler.application.JobSchedulerService; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Service +@RequiredArgsConstructor +public class PerformanceModifyService { + + private final PerformanceRepository performanceRepository; + private final ScheduleRepository scheduleRepository; + private final MemberRepository memberRepository; + private final CastRepository castRepository; + private final StaffRepository staffRepository; + private final BookingRepository bookingRepository; + private final PerformanceImageRepository performanceImageRepository; + private final JobSchedulerService jobSchedulerService; + + @Transactional + public PerformanceModifyResponse modifyPerformance(Long memberId, PerformanceModifyRequest request) { + log.info("Starting updatePerformance for memberId: {}, performanceId: {}", memberId, request.performanceId()); + + Member member = validateMember(memberId); + Long userId = member.getUser().getId(); + + Performance performance = findPerformance(request.performanceId()); + + validateOwnership(userId, performance); + + List scheduleIds = scheduleRepository.findIdsByPerformanceId(request.performanceId()); + boolean isBookerExist = bookingRepository.existsByScheduleIdIn(scheduleIds); + + if (isBookerExist && request.ticketPrice() != performance.getTicketPrice()) { + log.error("Ticket price update failed due to existing bookings for performanceId: {}", performance.getId()); + throw new BadRequestException(PerformanceErrorCode.PRICE_UPDATE_NOT_ALLOWED); + } + + updatePerformanceDetails(performance, request, isBookerExist); + + List modifiedSchedules = processSchedules(request.scheduleModifyRequests(), + performance); + List modifiedCasts = processCasts(request.castModifyRequests(), performance); + List modifiedStaffs = processStaffs(request.staffModifyRequests(), performance); + List modifiedPerformanceImages = processPerformanceImages( + request.performanceImageModifyRequests(), performance); + + PerformanceModifyResponse response = completeModifyResponse(performance, modifiedSchedules, modifiedCasts, + modifiedStaffs, modifiedPerformanceImages); + + log.info("Successfully completed updatePerformance for performanceId: {}", request.performanceId()); + return response; + } + + private Member validateMember(Long memberId) { + log.debug("Validating memberId: {}", memberId); + return memberRepository.findById(memberId) + .orElseThrow(() -> { + log.error("Member not found: memberId: {}", memberId); + return new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND); + }); + } + + private Performance findPerformance(Long performanceId) { + log.debug("Finding performance with performanceId: {}", performanceId); + return performanceRepository.findById(performanceId) + .orElseThrow(() -> { + log.error("Performance not found: performanceId: {}", performanceId); + return new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND); + }); + } + + private void validateOwnership(Long userId, Performance performance) { + if (!performance.getUsers().getId().equals(userId)) { + log.error("User ID {} does not own performance ID {}", userId, performance.getId()); + throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); + } + } + + private void updatePerformanceDetails(Performance performance, PerformanceModifyRequest request, + boolean isBookerExist) { + log.debug("Updating performance details for performanceId: {}", performance.getId()); + + performance.update( + 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.totalScheduleCount() + ); + + if (!isBookerExist) { + log.debug("Updating ticket price to {}", request.ticketPrice()); + performance.updateTicketPrice(request.ticketPrice()); + } + + performanceRepository.save(performance); + log.debug("Performance details updated for performanceId: {}", performance.getId()); + } + + private List processSchedules(List scheduleRequests, + Performance performance) { + List existingScheduleIds = scheduleRepository.findIdsByPerformanceId(performance.getId()); + + List requestScheduleIds = scheduleRequests.stream() + .map(ScheduleModifyRequest::scheduleId) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + + List schedulesToDelete = existingScheduleIds.stream() + .filter(id -> !requestScheduleIds.contains(id)) + .collect(Collectors.toList()); + + deleteRemainingSchedules(schedulesToDelete); + + List schedules = scheduleRequests.stream() + .map(request -> { + Schedule schedule; + if (request.scheduleId() == null) { + schedule = addSchedule(request, performance); + } else { + schedule = updateSchedule(request, performance); + } + + // isBooking이 true인 경우만 스케줄러에 등록 + if (schedule.isBooking()) { + jobSchedulerService.addScheduleIfNotExists(schedule); + } + + return schedule; + }) + .collect(Collectors.toList()); + + assignScheduleNumbers(schedules); + + return schedules.stream() + .map(schedule -> ScheduleModifyResponse.of( + schedule.getId(), + schedule.getPerformanceDate(), + schedule.getTotalTicketCount(), + calculateDueDate(schedule.getPerformanceDate()), + schedule.getScheduleNumber() + )) + .collect(Collectors.toList()); + } + + private void assignScheduleNumbers(List schedules) { + schedules.sort(Comparator.comparing(Schedule::getPerformanceDate)); + + for (int i = 0; i < schedules.size(); i++) { + ScheduleNumber scheduleNumber = ScheduleNumber.values()[i]; + schedules.get(i).updateScheduleNumber(scheduleNumber); + } + } + + private Schedule addSchedule(ScheduleModifyRequest request, Performance performance) { + log.debug("Adding schedules for performanceId: {}", performance.getId()); + + if (request.performanceDate().isBefore(LocalDateTime.now())) { + throw new BadRequestException(PerformanceErrorCode.PAST_SCHEDULE_NOT_ALLOWED); + } + + long existingSchedulesCount = scheduleRepository.countByPerformanceId(performance.getId()); + + if ((existingSchedulesCount + 1) > 10) { + throw new BadRequestException(PerformanceErrorCode.MAX_SCHEDULE_LIMIT_EXCEEDED); + } + + Schedule schedule = Schedule.create( + request.performanceDate(), + request.totalTicketCount(), + 0, + true, + ScheduleNumber.FIRST, // 임시로 1회차 + performance + ); + + Schedule savedSchedule = scheduleRepository.save(schedule); + log.debug("Added schedule with scheduleId: {} for performanceId: {}", savedSchedule.getId(), + performance.getId()); + return savedSchedule; + } + + private Schedule updateSchedule(ScheduleModifyRequest request, Performance performance) { + log.debug("Updating schedules for scheduleId: {}", request.scheduleId()); + + Schedule schedule = scheduleRepository.findById(request.scheduleId()) + .orElseThrow(() -> { + log.error("Schedule not found: scheduleId: {}", request.scheduleId()); + return new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND); + }); + + if (!schedule.getPerformance().equals(performance)) { + throw new ForbiddenException(ScheduleErrorCode.SCHEDULE_NOT_BELONG_TO_PERFORMANCE); + } + + // 종료된 스케줄(기존 스케쥴 날짜가 과거인 경우)은 날짜 변경 불가 + if (schedule.getPerformanceDate().isBefore(LocalDateTime.now())) { + if (!schedule.getPerformanceDate().isEqual(request.performanceDate())) { + throw new BadRequestException( + PerformanceErrorCode.SCHEDULE_MODIFICATION_NOT_ALLOWED_FOR_ENDED_SCHEDULE); + } + // 날짜 변경이 없으면 그대로 반환 + return schedule; + } + + // 종료되지 않은 스케줄을 과거 날짜로 수정 시 에러 발생 + if (request.performanceDate().isBefore(LocalDateTime.now())) { + throw new BadRequestException(PerformanceErrorCode.PAST_SCHEDULE_NOT_ALLOWED); + } + + // 티켓 수 관련 검증 + if (request.totalTicketCount() != schedule.getTotalTicketCount()) { + // 판매된 티켓 수보다 적은 totalTicketCount로 변경하려는 경우 예외 처리 + if (request.totalTicketCount() < schedule.getSoldTicketCount()) { + throw new BadRequestException(PerformanceErrorCode.INVALID_TICKET_COUNT); + } + + // 매진 상태로 변경 (soldTicketCount와 totalTicketCount가 동일하고, 기존 isBooking이 true인 경우) + if (request.totalTicketCount() == schedule.getSoldTicketCount() && schedule.isBooking()) { + schedule.updateIsBooking(false); // 매진 처리 (isBooking = false) + } + + // 매진이 풀리는 경우 (totalTicketCount 증가, 기존 isBooking이 false인 경우) + else if (request.totalTicketCount() > schedule.getTotalTicketCount() && !schedule.isBooking()) { + schedule.updateIsBooking(true); // 매진 풀림 처리 (isBooking = true) + } + } + + jobSchedulerService.cancelScheduledTaskForPerformance(schedule); + + schedule.update( + request.performanceDate(), + request.totalTicketCount(), + schedule.getScheduleNumber() // 기존 scheduleNumber 유지 + ); + return scheduleRepository.save(schedule); + } + + private void deleteRemainingSchedules(List scheduleIds) { + if (scheduleIds == null || scheduleIds.isEmpty()) { + log.debug("No schedules to delete"); + return; + } + + scheduleIds.forEach(scheduleId -> { + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> { + log.error("Schedule not found: scheduleId: {}", scheduleId); + return new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND); + }); + + jobSchedulerService.cancelScheduledTaskForPerformance(schedule); + + scheduleRepository.delete(schedule); + log.debug("Deleted schedule with scheduleId: {}", scheduleId); + }); + } + + private List processCasts(List castRequests, Performance performance) { + log.debug("Processing casts for performanceId: {}", performance.getId()); + + List existingCastIds = castRepository.findIdsByPerformanceId(performance.getId()); + + List responses = castRequests.stream() + .map(request -> { + if (request.castId() == null) { + return addCast(request, performance); + } else { + existingCastIds.remove(request.castId()); + return updateCast(request, performance); + } + }) + .toList(); + + deleteRemainingCasts(existingCastIds); + + return responses; + } + + private CastModifyResponse addCast(CastModifyRequest request, Performance performance) { + log.debug("Adding casts for performanceId: {}", performance.getId()); + + Cast cast = Cast.create( + request.castName(), + request.castRole(), + request.castPhoto(), + performance + ); + Cast savedCast = castRepository.save(cast); + log.debug("Added cast with castId: {} for performanceId: {}", savedCast.getId(), performance.getId()); + return CastModifyResponse.of( + savedCast.getId(), + savedCast.getCastName(), + savedCast.getCastRole(), + savedCast.getCastPhoto() + ); + } + + private CastModifyResponse updateCast(CastModifyRequest request, Performance performance) { + log.debug("Updating casts for castId: {}", request.castId()); + + Cast cast = castRepository.findById(request.castId()) + .orElseThrow(() -> { + log.error("Cast not found: castId: {}", request.castId()); + return new NotFoundException(CastErrorCode.CAST_NOT_FOUND); + }); + + if (!cast.getPerformance().equals(performance)) { + throw new ForbiddenException(CastErrorCode.CAST_NOT_BELONG_TO_PERFORMANCE); + } + + cast.update( + request.castName(), + request.castRole(), + request.castPhoto() + ); + castRepository.save(cast); + log.debug("Updated cast with castId: {}", cast.getId()); + return CastModifyResponse.of( + cast.getId(), + cast.getCastName(), + cast.getCastRole(), + cast.getCastPhoto() + ); + } + + private void deleteRemainingCasts(List castIds) { + if (castIds == null || castIds.isEmpty()) { + log.debug("No casts to delete"); + return; + } + + castIds.forEach(castId -> { + Cast cast = castRepository.findById(castId) + .orElseThrow(() -> { + log.error("Cast not found: castId: {}", castId); + return new NotFoundException(CastErrorCode.CAST_NOT_FOUND); + }); + castRepository.delete(cast); + log.debug("Deleted cast with castId: {}", castId); + }); + } + + private List processStaffs(List staffRequests, Performance performance) { + log.debug("Processing staffs for performanceId: {}", performance.getId()); + + List existingStaffIds = staffRepository.findIdsByPerformanceId(performance.getId()); + + List responses = staffRequests.stream() + .map(request -> { + if (request.staffId() == null) { + return addStaff(request, performance); + } else { + existingStaffIds.remove(request.staffId()); // 요청에 포함된 ID는 삭제 후보에서 제거 + return updateStaff(request, performance); + } + }) + .collect(Collectors.toList()); + + deleteRemainingStaffs(existingStaffIds); + + return responses; + } + + private StaffModifyResponse addStaff(StaffModifyRequest request, Performance performance) { + log.debug("Adding staffs for performanceId: {}", performance.getId()); + + Staff staff = Staff.create( + request.staffName(), + request.staffRole(), + request.staffPhoto(), + performance + ); + Staff savedStaff = staffRepository.save(staff); + log.debug("Added staff with staffId: {} for performanceId: {}", savedStaff.getId(), performance.getId()); + return StaffModifyResponse.of( + savedStaff.getId(), + savedStaff.getStaffName(), + savedStaff.getStaffRole(), + savedStaff.getStaffPhoto() + ); + } + + private StaffModifyResponse updateStaff(StaffModifyRequest request, Performance performance) { + log.debug("Updating staffs for staffId: {}", request.staffId()); + + Staff staff = staffRepository.findById(request.staffId()) + .orElseThrow(() -> { + log.error("Staff not found: staffId: {}", request.staffId()); + return new NotFoundException(StaffErrorCode.STAFF_NOT_FOUND); + }); + + if (!staff.getPerformance().equals(performance)) { + throw new ForbiddenException(StaffErrorCode.STAFF_NOT_BELONG_TO_PERFORMANCE); + } + + staff.update( + request.staffName(), + request.staffRole(), + request.staffPhoto() + ); + staffRepository.save(staff); + log.debug("Updated staff with staffId: {}", staff.getId()); + return StaffModifyResponse.of( + staff.getId(), + staff.getStaffName(), + staff.getStaffRole(), + staff.getStaffPhoto() + ); + } + + private void deleteRemainingStaffs(List staffIds) { + if (staffIds == null || staffIds.isEmpty()) { + log.debug("No staffs to delete"); + return; + } + + staffIds.forEach(staffId -> { + Staff staff = staffRepository.findById(staffId) + .orElseThrow(() -> { + log.error("Staff not found: staffId: {}", staffId); + return new NotFoundException(StaffErrorCode.STAFF_NOT_FOUND); + }); + staffRepository.delete(staff); + log.debug("Deleted staff with staffId: {}", staffId); + }); + } + + private int calculateDueDate(LocalDateTime performanceDate) { + return (int)ChronoUnit.DAYS.between(LocalDate.now(), performanceDate.toLocalDate()); + } + + private List processPerformanceImages( + List performanceImageRequests, Performance performance) { + log.debug("Processing performanceImages for performanceId: {}", performance.getId()); + + List existingPerformanceImageIds = performanceImageRepository.findIdsByPerformanceId(performance.getId()); + + List responses = performanceImageRequests.stream() + .map(request -> { + if (request.performanceImageId() == null) { + return addPerformanceImage(request, performance); + } else { + existingPerformanceImageIds.remove(request.performanceImageId()); + return updatePerformanceImage(request, performance); + } + }) + .toList(); + + deleteRemainingPerformanceImages(existingPerformanceImageIds); + + return responses; + } + + private PerformanceImageModifyResponse addPerformanceImage(PerformanceImageModifyRequest request, + Performance performance) { + log.debug("Adding performanceImages for performanceId: {}", performance.getId()); + + PerformanceImage performanceImage = PerformanceImage.create( + request.performanceImage(), + performance + ); + PerformanceImage savedPerformanceImage = performanceImageRepository.save(performanceImage); + log.debug("Added performanceImage: {}", savedPerformanceImage.getId()); + return PerformanceImageModifyResponse.of( + savedPerformanceImage.getId(), + savedPerformanceImage.getPerformanceImage() + ); + } + + private PerformanceImageModifyResponse updatePerformanceImage(PerformanceImageModifyRequest request, + Performance performance) { + log.debug("Updating performanceImages for performanceId: {}", performance.getId()); + + PerformanceImage performanceImage = performanceImageRepository.findById(request.performanceImageId()) + .orElseThrow(() -> { + log.error("PerformanceImage not found: performanceId: {}", request.performanceImageId()); + return new NotFoundException(PerformanceImageErrorCode.PERFORMANCE_IMAGE_NOT_FOUND); + }); + + if (!performanceImage.getPerformance().equals(performance)) { + throw new ForbiddenException(PerformanceImageErrorCode.PERFORMANCE_IMAGE_NOT_BELONG_TO_PERFORMANCE); + } + + performanceImage.update( + request.performanceImage() + ); + performanceImageRepository.save(performanceImage); + log.debug("Updated performanceImage: {}", performanceImage.getId()); + return PerformanceImageModifyResponse.of( + performanceImage.getId(), + performanceImage.getPerformanceImage() + ); + } + + private void deleteRemainingPerformanceImages(List performanceImageIds) { + if (performanceImageIds == null || performanceImageIds.isEmpty()) { + log.debug("No performanceImages to delete"); + return; + } + + performanceImageIds.forEach(performanceImageId -> { + PerformanceImage performanceImage = performanceImageRepository.findById(performanceImageId) + .orElseThrow(() -> { + log.error("PerformanceImage not found: performanceImageId: {}", performanceImageId); + return new NotFoundException(PerformanceImageErrorCode.PERFORMANCE_IMAGE_NOT_FOUND); + }); + performanceImageRepository.delete(performanceImage); + log.debug("Deleted performanceImage: {}", performanceImageId); + }); + } + + private PerformanceModifyResponse completeModifyResponse( + Performance performance, + List scheduleModifyResponses, + List castModifyResponses, + List staffModifyResponses, + List performanceImageModifyResponses + ) { + log.debug("Creating PerformanceModifyResponse for performanceId: {}", performance.getId()); + PerformanceModifyResponse response = PerformanceModifyResponse.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(), + scheduleModifyResponses, + castModifyResponses, + staffModifyResponses, + performanceImageModifyResponses + ); + log.info("PerformanceModifyResponse created successfully for performanceId: {}", performance.getId()); + return response; + } +} \ 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 1907a176..e1352dda 100644 --- a/src/main/java/com/beat/domain/performance/application/PerformanceService.java +++ b/src/main/java/com/beat/domain/performance/application/PerformanceService.java @@ -1,25 +1,43 @@ package com.beat.domain.performance.application; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.temporal.ChronoUnit; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + import com.beat.domain.booking.dao.BookingRepository; +import com.beat.domain.cast.dao.CastRepository; import com.beat.domain.cast.domain.Cast; 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.application.dto.bookingPerformanceDetail.BookingPerformanceDetailResponse; +import com.beat.domain.performance.application.dto.bookingPerformanceDetail.BookingPerformanceDetailScheduleResponse; import com.beat.domain.performance.application.dto.create.CastResponse; +import com.beat.domain.performance.application.dto.create.PerformanceImageResponse; import com.beat.domain.performance.application.dto.create.ScheduleResponse; import com.beat.domain.performance.application.dto.create.StaffResponse; -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.application.dto.makerPerformance.MakerPerformanceDetailResponse; +import com.beat.domain.performance.application.dto.makerPerformance.MakerPerformanceResponse; +import com.beat.domain.performance.application.dto.modify.PerformanceModifyDetailResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailCastResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailImageResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailScheduleResponse; +import com.beat.domain.performance.application.dto.performanceDetail.PerformanceDetailStaffResponse; +import com.beat.domain.performance.dao.PerformanceImageRepository; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.Performance; +import com.beat.domain.performance.domain.PerformanceImage; 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.performance.port.in.PerformanceUseCase; 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.domain.staff.domain.Staff; @@ -28,276 +46,189 @@ import com.beat.domain.user.exception.UserErrorCode; import com.beat.global.common.exception.ForbiddenException; 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.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +@Slf4j @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; - private final MemberRepository memberRepository; - private final UserRepository userRepository; - private final BookingRepository bookingRepository; - - @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 - ); - } - - @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(), - performance.getBankName() != null ? performance.getBankName().name() : null, - performance.getAccountNumber(), - performance.getAccountHolder() - ); - } - - @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()); - } - - @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); - } - - @Transactional - public PerformanceEditResponse getPerformanceEdit(Long memberId, Long performanceId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); - - Long userId = member.getUser().getId(); - - Performance performance = performanceRepository.findById(performanceId) - .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); - - if (!performance.getUsers().getId().equals(userId)) { - throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); - } - - List scheduleIds = scheduleRepository.findIdsByPerformanceId(performanceId); - - boolean isBookerExist = bookingRepository.existsByScheduleIdIn(scheduleIds); - - List schedules = scheduleRepository.findAllByPerformanceId(performanceId); - List casts = castRepository.findAllByPerformanceId(performanceId); - List staffs = staffRepository.findAllByPerformanceId(performanceId); - - return mapToPerformanceEditResponse(performance, schedules, casts, staffs, isBookerExist); - } - - private PerformanceEditResponse mapToPerformanceEditResponse(Performance performance, List schedules, List casts, List staffs, boolean isBookerExist) { - List scheduleResponses = schedules.stream() - .map(schedule -> ScheduleResponse.of( - schedule.getId(), - schedule.getPerformanceDate(), - schedule.getTotalTicketCount(), - calculateDueDate(schedule.getPerformanceDate()), - 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 PerformanceEditResponse.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(), - isBookerExist, - scheduleResponses, - castResponses, - staffResponses - ); - } - - private int calculateDueDate(LocalDateTime performanceDate) { - return (int) ChronoUnit.DAYS.between(LocalDate.now(), performanceDate.toLocalDate()); - } +public class PerformanceService implements PerformanceUseCase { + private final PerformanceRepository performanceRepository; + private final ScheduleRepository scheduleRepository; + private final CastRepository castRepository; + private final StaffRepository staffRepository; + private final ScheduleService scheduleService; + private final MemberRepository memberRepository; + private final UserRepository userRepository; + private final BookingRepository bookingRepository; + private final PerformanceImageRepository performanceImageRepository; + + @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 -> { + int dueDate = scheduleService.calculateDueDate(schedule); + return PerformanceDetailScheduleResponse.of(schedule.getId(), schedule.getPerformanceDate(), + schedule.getScheduleNumber().name(), dueDate, schedule.isBooking()); + }) + .collect(Collectors.toList()); + + int minDueDate = scheduleService.getMinDueDate(scheduleRepository.findAllByPerformanceId(performanceId)); + + List castList = castRepository.findByPerformanceId(performanceId) + .stream() + .map(cast -> PerformanceDetailCastResponse.of(cast.getId(), cast.getCastName(), cast.getCastRole(), + cast.getCastPhoto())) + .collect(Collectors.toList()); + + List staffList = staffRepository.findByPerformanceId(performanceId) + .stream() + .map(staff -> PerformanceDetailStaffResponse.of(staff.getId(), staff.getStaffName(), staff.getStaffRole(), + staff.getStaffPhoto())) + .collect(Collectors.toList()); + + List performanceImageList = performanceImageRepository.findAllByPerformanceId( + performanceId) + .stream() + .map(image -> PerformanceDetailImageResponse.of(image.getId(), image.getPerformanceImage())) + .collect(Collectors.toList()); + + log.info("Successfully completed getPerformanceDetail for performanceId: {}", performanceId); + 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, minDueDate, performanceImageList); + } + + @Transactional(readOnly = true) + 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 -> { + int dueDate = scheduleService.calculateDueDate(schedule); + return BookingPerformanceDetailScheduleResponse.of(schedule.getId(), schedule.getPerformanceDate(), + schedule.getScheduleNumber().name(), scheduleService.getAvailableTicketCount(schedule), + schedule.isBooking(), dueDate); + }).collect(Collectors.toList()); + + return BookingPerformanceDetailResponse.of(performance.getId(), performance.getPerformanceTitle(), + performance.getPerformancePeriod(), scheduleList, performance.getTicketPrice(), + performance.getGenre().name(), performance.getPosterImage(), performance.getPerformanceVenue(), + performance.getPerformanceTeamName(), + performance.getBankName() != null ? performance.getBankName().name() : null, performance.getAccountNumber(), + performance.getAccountHolder()); + } + + @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 -> { + List schedules = scheduleRepository.findByPerformanceId(performance.getId()); + int minDueDate = scheduleService.getMinDueDate(schedules); + + return MakerPerformanceDetailResponse.of(performance.getId(), performance.getGenre().name(), + performance.getPerformanceTitle(), performance.getPosterImage(), performance.getPerformancePeriod(), + minDueDate); + }).collect(Collectors.toList()); + + List positiveDueDates = performanceDetails.stream() + .filter(detail -> detail.minDueDate() >= 0) + .sorted(Comparator.comparingInt(MakerPerformanceDetailResponse::minDueDate)) + .collect(Collectors.toList()); + + List negativeDueDates = performanceDetails.stream() + .filter(detail -> detail.minDueDate() < 0) + .sorted(Comparator.comparingInt(MakerPerformanceDetailResponse::minDueDate).reversed()) + .collect(Collectors.toList()); + + positiveDueDates.addAll(negativeDueDates); + + return MakerPerformanceResponse.of(user.getId(), positiveDueDates); + } + + @Override + @Transactional(readOnly = true) + public Performance findById(Long performanceId) { + return performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + } + + @Transactional + public PerformanceModifyDetailResponse getPerformanceEdit(Long memberId, Long performanceId) { + Member member = memberRepository.findById(memberId) + .orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); + + Long userId = member.getUser().getId(); + + Performance performance = performanceRepository.findById(performanceId) + .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); + + if (!performance.getUsers().getId().equals(userId)) { + throw new ForbiddenException(PerformanceErrorCode.NOT_PERFORMANCE_OWNER); + } + + List scheduleIds = scheduleRepository.findIdsByPerformanceId(performanceId); + + boolean isBookerExist = bookingRepository.existsByScheduleIdIn(scheduleIds); + + List schedules = scheduleRepository.findAllByPerformanceId(performanceId); + List casts = castRepository.findAllByPerformanceId(performanceId); + List staffs = staffRepository.findAllByPerformanceId(performanceId); + List performanceImages = performanceImageRepository.findAllByPerformanceId(performanceId); + + return mapToPerformanceEditResponse(performance, schedules, casts, staffs, performanceImages, isBookerExist); + } + + private PerformanceModifyDetailResponse mapToPerformanceEditResponse(Performance performance, + List schedules, List casts, List staffs, List performanceImages, + boolean isBookerExist) { + List scheduleResponses = schedules.stream() + .map(schedule -> ScheduleResponse.of(schedule.getId(), schedule.getPerformanceDate(), + schedule.getTotalTicketCount(), calculateDueDate(schedule.getPerformanceDate()), + 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()); + + List performanceImageResponses = performanceImages.stream() + .map(performanceImage -> PerformanceImageResponse.of(performanceImage.getId(), + performanceImage.getPerformanceImage())) + .collect(Collectors.toList()); + + return PerformanceModifyDetailResponse.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(), isBookerExist, scheduleResponses, castResponses, staffResponses, + performanceImageResponses); + } + + private int calculateDueDate(LocalDateTime performanceDate) { + return (int)ChronoUnit.DAYS.between(LocalDate.now(), performanceDate.toLocalDate()); + } } diff --git a/src/main/java/com/beat/domain/performance/application/PerformanceUpdateService.java b/src/main/java/com/beat/domain/performance/application/PerformanceUpdateService.java deleted file mode 100644 index 70a4c993..00000000 --- a/src/main/java/com/beat/domain/performance/application/PerformanceUpdateService.java +++ /dev/null @@ -1,165 +0,0 @@ -package com.beat.domain.performance.application; - -import com.beat.domain.cast.dao.CastRepository; -import com.beat.domain.cast.domain.Cast; -import com.beat.domain.cast.exception.CastErrorCode; -import com.beat.domain.member.dao.MemberRepository; -import com.beat.domain.member.exception.MemberErrorCode; -import com.beat.domain.performance.application.dto.update.*; -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.schedule.domain.Schedule; -import com.beat.domain.schedule.domain.ScheduleNumber; -import com.beat.domain.schedule.exception.ScheduleErrorCode; -import com.beat.domain.staff.dao.StaffRepository; -import com.beat.domain.staff.domain.Staff; -import com.beat.domain.staff.exception.StaffErrorCode; -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.LocalDateTime; -import java.time.temporal.ChronoUnit; -import java.util.List; -import java.util.stream.Collectors; - -@Service -@RequiredArgsConstructor -public class PerformanceUpdateService { - private final PerformanceRepository performanceRepository; - private final ScheduleRepository scheduleRepository; - private final MemberRepository memberRepository; - private final CastRepository castRepository; - private final StaffRepository staffRepository; - - @Transactional - public PerformanceUpdateResponse updatePerformance(Long memberId, PerformanceUpdateRequest request) { - memberRepository.findById(memberId).orElseThrow(() -> new NotFoundException(MemberErrorCode.MEMBER_NOT_FOUND)); - - Performance performance = performanceRepository.findById(request.performanceId()) - .orElseThrow(() -> new NotFoundException(PerformanceErrorCode.PERFORMANCE_NOT_FOUND)); - - performance.update( - 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.totalScheduleCount() - ); - performanceRepository.save(performance); - - List schedules = request.scheduleList().stream() - .map(scheduleRequest -> { - Schedule schedule = scheduleRepository.findById(scheduleRequest.scheduleId()) - .orElseThrow(() -> new NotFoundException(ScheduleErrorCode.NO_SCHEDULE_FOUND)); - schedule.update( - scheduleRequest.performanceDate(), - scheduleRequest.totalTicketCount(), - ScheduleNumber.valueOf(scheduleRequest.scheduleNumber()) - ); - return schedule; - }) - .collect(Collectors.toList()); - scheduleRepository.saveAll(schedules); - - List casts = request.castList().stream() - .map(castRequest -> { - Cast cast = castRepository.findById(castRequest.castId()) - .orElseThrow(() -> new NotFoundException(CastErrorCode.CAST_NOT_FOUND)); - cast.update( - castRequest.castName(), - castRequest.castRole(), - castRequest.castPhoto() - ); - return cast; - }) - .collect(Collectors.toList()); - castRepository.saveAll(casts); - - List staffs = request.staffList().stream() - .map(staffRequest -> { - Staff staff = staffRepository.findById(staffRequest.staffId()) - .orElseThrow(() -> new NotFoundException(StaffErrorCode.STAFF_NOT_FOUND)); - staff.update( - staffRequest.staffName(), - staffRequest.staffRole(), - staffRequest.staffPhoto() - ); - return staff; - }) - .collect(Collectors.toList()); - staffRepository.saveAll(staffs); - - return mapToPerformanceResponse(performance, schedules, casts, staffs); - } - - private PerformanceUpdateResponse mapToPerformanceResponse(Performance performance, List schedules, List casts, List staffs) { - List scheduleResponses = schedules.stream() - .map(schedule -> ScheduleUpdateResponse.of( - schedule.getId(), - schedule.getPerformanceDate(), - schedule.getTotalTicketCount(), - calculateDueDate(schedule.getPerformanceDate()), - schedule.getScheduleNumber() - )) - .collect(Collectors.toList()); - - List castResponses = casts.stream() - .map(cast -> CastUpdateResponse.of( - cast.getId(), - cast.getCastName(), - cast.getCastRole(), - cast.getCastPhoto() - )) - .collect(Collectors.toList()); - - List staffResponses = staffs.stream() - .map(staff -> StaffUpdateResponse.of( - staff.getId(), - staff.getStaffName(), - staff.getStaffRole(), - staff.getStaffPhoto() - )) - .collect(Collectors.toList()); - - return PerformanceUpdateResponse.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(LocalDateTime performanceDate) { - return (int) ChronoUnit.DAYS.between(LocalDate.now(), performanceDate.toLocalDate()); - } -} \ No newline at end of file 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 deleted file mode 100644 index 0cbe3616..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailSchedule.java +++ /dev/null @@ -1,16 +0,0 @@ -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/application/dto/MakerPerformanceDetail.java b/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java deleted file mode 100644 index 21dcaa61..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceDetail.java +++ /dev/null @@ -1,18 +0,0 @@ -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/PerformanceDetailCast.java b/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java deleted file mode 100644 index 2ce813eb..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailCast.java +++ /dev/null @@ -1,12 +0,0 @@ -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 deleted file mode 100644 index a8f48a30..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailResponse.java +++ /dev/null @@ -1,43 +0,0 @@ -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 deleted file mode 100644 index dcfe54c7..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailSchedule.java +++ /dev/null @@ -1,13 +0,0 @@ -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 deleted file mode 100644 index cf088eaf..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/PerformanceDetailStaff.java +++ /dev/null @@ -1,12 +0,0 @@ -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/application/dto/BookingPerformanceDetailResponse.java b/src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailResponse.java similarity index 82% rename from src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java rename to src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailResponse.java index 24dcf02c..d42afafa 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/BookingPerformanceDetailResponse.java +++ b/src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailResponse.java @@ -1,4 +1,4 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.bookingPerformanceDetail; import java.util.List; @@ -6,7 +6,7 @@ public record BookingPerformanceDetailResponse( Long performanceId, String performanceTitle, String performancePeriod, - List scheduleList, + List scheduleList, int ticketPrice, String genre, String posterImage, @@ -20,7 +20,7 @@ public static BookingPerformanceDetailResponse of( Long performanceId, String performanceTitle, String performancePeriod, - List scheduleList, + List scheduleList, int ticketPrice, String genre, String posterImage, diff --git a/src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailScheduleResponse.java b/src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailScheduleResponse.java new file mode 100644 index 00000000..4181e1bb --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/bookingPerformanceDetail/BookingPerformanceDetailScheduleResponse.java @@ -0,0 +1,17 @@ +package com.beat.domain.performance.application.dto.bookingPerformanceDetail; + +import java.time.LocalDateTime; + +public record BookingPerformanceDetailScheduleResponse( + Long scheduleId, + LocalDateTime performanceDate, + String scheduleNumber, + int availableTicketCount, + boolean isBooking, + int dueDate +) { + public static BookingPerformanceDetailScheduleResponse of(Long scheduleId, LocalDateTime performanceDate, String scheduleNumber, int availableTicketCount, boolean isBooking, int dueDate) { + return new BookingPerformanceDetailScheduleResponse(scheduleId, performanceDate, scheduleNumber, availableTicketCount, isBooking, dueDate); + } + +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageRequest.java b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageRequest.java new file mode 100644 index 00000000..6081829a --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageRequest.java @@ -0,0 +1,6 @@ +package com.beat.domain.performance.application.dto.create; + +public record PerformanceImageRequest( + String performanceImage +) { +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageResponse.java b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageResponse.java new file mode 100644 index 00000000..abf14ff3 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/create/PerformanceImageResponse.java @@ -0,0 +1,16 @@ +package com.beat.domain.performance.application.dto.create; + +public record PerformanceImageResponse( + Long imageId, + String imageUrl +) { + public static PerformanceImageResponse of( + Long imageId, + String imageUrl + ) { + return new PerformanceImageResponse( + imageId, + imageUrl + ); + } +} 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 index 1ae592f9..2663abf6 100644 --- 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 @@ -23,5 +23,6 @@ public record PerformanceRequest( int totalScheduleCount, List scheduleList, List castList, - List staffList + List staffList, + List performanceImageList ) {} \ 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 index 9d34b8da..4fbbbf0b 100644 --- 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 @@ -25,7 +25,8 @@ public record PerformanceResponse( int totalScheduleCount, List scheduleList, List castList, - List staffList + List staffList, + List performanceImageList ) { public static PerformanceResponse of( Long userId, @@ -47,7 +48,8 @@ public static PerformanceResponse of( int totalScheduleCount, List scheduleList, List castList, - List staffList + List staffList, + List performanceImageList ) { return new PerformanceResponse( userId, @@ -69,7 +71,8 @@ public static PerformanceResponse of( totalScheduleCount, scheduleList, castList, - staffList + staffList, + performanceImageList ); } } diff --git a/src/main/java/com/beat/domain/performance/application/dto/home/HomeFindAllResponse.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomeFindAllResponse.java new file mode 100644 index 00000000..1117fd4b --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomeFindAllResponse.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto.home; + +import java.util.List; + +public record HomeFindAllResponse( + List promotionList, + List performanceList +) { + public static HomeFindAllResponse of(List promotionList, List performanceList) { + return new HomeFindAllResponse(promotionList, performanceList); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomeFindRequest.java similarity index 70% rename from src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java rename to src/main/java/com/beat/domain/performance/application/dto/home/HomeFindRequest.java index 841c8f08..3431ad80 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/home/HomeRequest.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomeFindRequest.java @@ -2,5 +2,5 @@ import com.beat.domain.performance.domain.Genre; -public record HomeRequest(Genre genre) { -} +public record HomeFindRequest(Genre genre) { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java index 1417cfb4..71dbefc4 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomePerformanceDetail.java @@ -1,16 +1,28 @@ package com.beat.domain.performance.application.dto.home; +import com.beat.domain.performance.domain.Performance; + public record HomePerformanceDetail( - Long performanceId, - String performanceTitle, - String performancePeriod, - int ticketPrice, - int dueDate, - String genre, - String posterImage, - String performanceVenue + 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); - } -} + + public static HomePerformanceDetail of(Performance performance, int minDueDate) { + return new HomePerformanceDetail( + performance.getId(), + performance.getPerformanceTitle(), + performance.getPerformancePeriod(), + performance.getTicketPrice(), + minDueDate, + performance.getGenre().name(), + performance.getPosterImage(), + performance.getPerformanceVenue() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java index 9bd9e35f..cf49a643 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java +++ b/src/main/java/com/beat/domain/performance/application/dto/home/HomePromotionDetail.java @@ -1,11 +1,19 @@ package com.beat.domain.performance.application.dto.home; -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); - } +import com.beat.domain.promotion.domain.CarouselNumber; +import com.beat.domain.promotion.domain.Promotion; + +public record HomePromotionDetail(Long promotionId, String promotionPhoto, Long performanceId, String redirectUrl, + boolean isExternal, CarouselNumber carouselNumber) { + + public static HomePromotionDetail from(Promotion promotion) { + Long performanceId = null; + + if (promotion.getPerformance() != null) { + performanceId = promotion.getPerformance().getId(); + } + + return new HomePromotionDetail(promotion.getId(), promotion.getPromotionPhoto(), performanceId, + promotion.getRedirectUrl(), promotion.isExternal(), promotion.getCarouselNumber()); + } } diff --git a/src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java b/src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java deleted file mode 100644 index c3d963b4..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/home/HomeResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.beat.domain.performance.application.dto.home; - -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/application/dto/makerPerformance/MakerPerformanceDetailResponse.java b/src/main/java/com/beat/domain/performance/application/dto/makerPerformance/MakerPerformanceDetailResponse.java new file mode 100644 index 00000000..f34501ad --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/makerPerformance/MakerPerformanceDetailResponse.java @@ -0,0 +1,20 @@ +package com.beat.domain.performance.application.dto.makerPerformance; + +public record MakerPerformanceDetailResponse( + Long performanceId, + String genre, + String performanceTitle, + String posterImage, + String performancePeriod, + int minDueDate +) { + public static MakerPerformanceDetailResponse of( + Long performanceId, + String genre, + String performanceTitle, + String posterImage, + String performancePeriod, + int minDueDate) { // minDueDate 매개변수 추가 + return new MakerPerformanceDetailResponse(performanceId, genre, performanceTitle, posterImage, performancePeriod, minDueDate); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java b/src/main/java/com/beat/domain/performance/application/dto/makerPerformance/MakerPerformanceResponse.java similarity index 55% rename from src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java rename to src/main/java/com/beat/domain/performance/application/dto/makerPerformance/MakerPerformanceResponse.java index 6a36683d..ef0f0ffd 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/MakerPerformanceResponse.java +++ b/src/main/java/com/beat/domain/performance/application/dto/makerPerformance/MakerPerformanceResponse.java @@ -1,14 +1,14 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.makerPerformance; import java.util.List; public record MakerPerformanceResponse( Long userId, - List performances + List performances ) { public static MakerPerformanceResponse of( Long userId, - List performances) { + List performances) { return new MakerPerformanceResponse(userId, performances); } } diff --git a/src/main/java/com/beat/domain/performance/application/dto/PerformanceEditResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyDetailResponse.java similarity index 80% rename from src/main/java/com/beat/domain/performance/application/dto/PerformanceEditResponse.java rename to src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyDetailResponse.java index e2341753..eb58f23c 100644 --- a/src/main/java/com/beat/domain/performance/application/dto/PerformanceEditResponse.java +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyDetailResponse.java @@ -1,6 +1,7 @@ -package com.beat.domain.performance.application.dto; +package com.beat.domain.performance.application.dto.modify; import com.beat.domain.performance.application.dto.create.CastResponse; +import com.beat.domain.performance.application.dto.create.PerformanceImageResponse; import com.beat.domain.performance.application.dto.create.ScheduleResponse; import com.beat.domain.performance.application.dto.create.StaffResponse; import com.beat.domain.performance.domain.BankName; @@ -8,7 +9,7 @@ import java.util.List; -public record PerformanceEditResponse( +public record PerformanceModifyDetailResponse( Long userId, Long performanceId, String performanceTitle, @@ -29,9 +30,10 @@ public record PerformanceEditResponse( boolean isBookerExist, List scheduleList, List castList, - List staffList + List staffList, + List performanceImageList ) { - public static PerformanceEditResponse of( + public static PerformanceModifyDetailResponse of( Long userId, Long performanceId, String performanceTitle, @@ -52,9 +54,10 @@ public static PerformanceEditResponse of( boolean isBookerExist, List scheduleList, List castList, - List staffList + List staffList, + List performanceImageList ) { - return new PerformanceEditResponse( + return new PerformanceModifyDetailResponse( userId, performanceId, performanceTitle, @@ -75,7 +78,8 @@ public static PerformanceEditResponse of( isBookerExist, scheduleList, castList, - staffList + staffList, + performanceImageList ); } } diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyRequest.java b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyRequest.java new file mode 100644 index 00000000..cd8b966a --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyRequest.java @@ -0,0 +1,33 @@ +package com.beat.domain.performance.application.dto.modify; + +import com.beat.domain.performance.application.dto.modify.cast.CastModifyRequest; +import com.beat.domain.performance.application.dto.modify.performanceImage.PerformanceImageModifyRequest; +import com.beat.domain.performance.application.dto.modify.schedule.ScheduleModifyRequest; +import com.beat.domain.performance.application.dto.modify.staff.StaffModifyRequest; +import com.beat.domain.performance.domain.BankName; +import com.beat.domain.performance.domain.Genre; + +import java.util.List; + +public record PerformanceModifyRequest( + 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 totalScheduleCount, + int ticketPrice, + List scheduleModifyRequests, + List castModifyRequests, + List staffModifyRequests, + List performanceImageModifyRequests +) {} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyResponse.java new file mode 100644 index 00000000..cd6dfdf2 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/PerformanceModifyResponse.java @@ -0,0 +1,83 @@ +package com.beat.domain.performance.application.dto.modify; + +import com.beat.domain.performance.application.dto.modify.cast.CastModifyResponse; +import com.beat.domain.performance.application.dto.modify.performanceImage.PerformanceImageModifyResponse; +import com.beat.domain.performance.application.dto.modify.schedule.ScheduleModifyResponse; +import com.beat.domain.performance.application.dto.modify.staff.StaffModifyResponse; +import com.beat.domain.performance.domain.BankName; +import com.beat.domain.performance.domain.Genre; + +import java.util.List; + +public record PerformanceModifyResponse( + 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 scheduleModifyResponses, + List castModifyResponses, + List staffModifyResponses, + List performanceImageModifyResponses +) { + public static PerformanceModifyResponse 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 scheduleModifyResponses, + List castModifyResponses, + List staffModifyResponses, + List performanceImageModifyResponses) + { + + return new PerformanceModifyResponse( + userId, + performanceId, + performanceTitle, + genre, + runningTime, + performanceDescription, + performanceAttentionNote, + bankName, + accountNumber, + accountHolder, + posterImage, + performanceTeamName, + performanceVenue, + performanceContact, + performancePeriod, + ticketPrice, + totalScheduleCount, + scheduleModifyResponses, + castModifyResponses, + staffModifyResponses, + performanceImageModifyResponses + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyRequest.java b/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyRequest.java new file mode 100644 index 00000000..3f3b56d2 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyRequest.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto.modify.cast; + +import org.jetbrains.annotations.Nullable; + +public record CastModifyRequest( + @Nullable + Long castId, + String castName, + String castRole, + String castPhoto +) { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyResponse.java new file mode 100644 index 00000000..bfed8151 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/cast/CastModifyResponse.java @@ -0,0 +1,11 @@ +package com.beat.domain.performance.application.dto.modify.cast; + +public record CastModifyResponse(Long castId, + String castName, + String castRole, + String castPhoto) { + + public static CastModifyResponse of(Long castId, String castName, String castRole, String castPhoto) { + return new CastModifyResponse(castId, castName, castRole, castPhoto); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyRequest.java b/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyRequest.java new file mode 100644 index 00000000..4e05250c --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyRequest.java @@ -0,0 +1,10 @@ +package com.beat.domain.performance.application.dto.modify.performanceImage; + +import javax.annotation.Nullable; + +public record PerformanceImageModifyRequest( + @Nullable + Long performanceImageId, + String performanceImage +) { +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyResponse.java new file mode 100644 index 00000000..18d93f89 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/performanceImage/PerformanceImageModifyResponse.java @@ -0,0 +1,11 @@ +package com.beat.domain.performance.application.dto.modify.performanceImage; + +public record PerformanceImageModifyResponse( + Long performanceImageId, + String performanceImage +) { + + public static PerformanceImageModifyResponse of(Long performanceImageId, String performanceImage) { + return new PerformanceImageModifyResponse(performanceImageId, performanceImage); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyRequest.java b/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyRequest.java new file mode 100644 index 00000000..fcda2a5f --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyRequest.java @@ -0,0 +1,13 @@ +package com.beat.domain.performance.application.dto.modify.schedule; + +import org.jetbrains.annotations.Nullable; + +import java.time.LocalDateTime; + +public record ScheduleModifyRequest( + @Nullable + Long scheduleId, + LocalDateTime performanceDate, + int totalTicketCount +) { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyResponse.java new file mode 100644 index 00000000..fadf4dc9 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/schedule/ScheduleModifyResponse.java @@ -0,0 +1,16 @@ +package com.beat.domain.performance.application.dto.modify.schedule; + +import com.beat.domain.schedule.domain.ScheduleNumber; + +import java.time.LocalDateTime; + +public record ScheduleModifyResponse(Long scheduleId, + LocalDateTime performanceDate, + int totalTicketCount, + int dueDate, + ScheduleNumber scheduleNumber) { + + public static ScheduleModifyResponse of(Long scheduleId, LocalDateTime performanceDate, int totalTicketCount, int dueDate, ScheduleNumber scheduleNumber) { + return new ScheduleModifyResponse(scheduleId, performanceDate, totalTicketCount, dueDate, scheduleNumber); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyRequest.java b/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyRequest.java new file mode 100644 index 00000000..67020ed1 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyRequest.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto.modify.staff; + +import org.jetbrains.annotations.Nullable; + +public record StaffModifyRequest( + @Nullable + Long staffId, + String staffName, + String staffRole, + String staffPhoto +) { +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyResponse.java b/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyResponse.java new file mode 100644 index 00000000..afe97f49 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/modify/staff/StaffModifyResponse.java @@ -0,0 +1,11 @@ +package com.beat.domain.performance.application.dto.modify.staff; + +public record StaffModifyResponse(Long staffId, + String staffName, + String staffRole, + String staffPhoto) { + + public static StaffModifyResponse of(Long staffId, String staffName, String staffRole, String staffPhoto) { + return new StaffModifyResponse(staffId, staffName, staffRole, staffPhoto); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailCastResponse.java b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailCastResponse.java new file mode 100644 index 00000000..ede5b7bc --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailCastResponse.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto.performanceDetail; + +public record PerformanceDetailCastResponse( + Long castId, + String castName, + String castRole, + String castPhoto +) { + public static PerformanceDetailCastResponse of(Long castId, String castName, String castRole, String castPhoto) { + return new PerformanceDetailCastResponse(castId, castName, castRole, castPhoto); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailImageResponse.java b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailImageResponse.java new file mode 100644 index 00000000..05def70f --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailImageResponse.java @@ -0,0 +1,10 @@ +package com.beat.domain.performance.application.dto.performanceDetail; + +public record PerformanceDetailImageResponse( + Long performanceImageId, + String performanceImage +) { + public static PerformanceDetailImageResponse of(Long performanceImageId, String performanceImage) { + return new PerformanceDetailImageResponse(performanceImageId, performanceImage); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailResponse.java b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailResponse.java new file mode 100644 index 00000000..2047176d --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailResponse.java @@ -0,0 +1,63 @@ +package com.beat.domain.performance.application.dto.performanceDetail; + +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, + int minDueDate, + List performanceImageList +) { + 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, + int minDueDate, + List performanceImageList + ) { + return new PerformanceDetailResponse( + performanceId, + performanceTitle, + performancePeriod, + scheduleList, + ticketPrice, + genre, + posterImage, + runningTime, + performanceVenue, + performanceDescription, + performanceAttentionNote, + performanceContact, + performanceTeamName, + castList, + staffList, + minDueDate, + performanceImageList + ); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailScheduleResponse.java b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailScheduleResponse.java new file mode 100644 index 00000000..4adbddd7 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailScheduleResponse.java @@ -0,0 +1,15 @@ +package com.beat.domain.performance.application.dto.performanceDetail; + +import java.time.LocalDateTime; + +public record PerformanceDetailScheduleResponse( + Long scheduleId, + LocalDateTime performanceDate, + String scheduleNumber, + int dueDate, + boolean isBooking +) { + public static PerformanceDetailScheduleResponse of(Long scheduleId, LocalDateTime performanceDate, String scheduleNumber, int dueDate, boolean isBooking) { + return new PerformanceDetailScheduleResponse(scheduleId, performanceDate, scheduleNumber, dueDate, isBooking); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailStaffResponse.java b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailStaffResponse.java new file mode 100644 index 00000000..8a24c9fc --- /dev/null +++ b/src/main/java/com/beat/domain/performance/application/dto/performanceDetail/PerformanceDetailStaffResponse.java @@ -0,0 +1,12 @@ +package com.beat.domain.performance.application.dto.performanceDetail; + +public record PerformanceDetailStaffResponse( + Long staffId, + String staffName, + String staffRole, + String staffPhoto +) { + public static PerformanceDetailStaffResponse of(Long staffId, String staffName, String staffRole, String staffPhoto) { + return new PerformanceDetailStaffResponse(staffId, staffName, staffRole, staffPhoto); + } +} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateRequest.java b/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateRequest.java deleted file mode 100644 index cb178956..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -public record CastUpdateRequest( - Long castId, - String castName, - String castRole, - String castPhoto -) {} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateResponse.java b/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateResponse.java deleted file mode 100644 index 39d8d97a..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/CastUpdateResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -public record CastUpdateResponse( - Long castId, - String castName, - String castRole, - String castPhoto -) { - public static CastUpdateResponse of(Long castId, String castName, String castRole, String castPhoto) { - return new CastUpdateResponse(castId, castName, castRole, castPhoto); - } -} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateRequest.java b/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateRequest.java deleted file mode 100644 index 255883c7..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateRequest.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -import com.beat.domain.performance.domain.BankName; -import com.beat.domain.performance.domain.Genre; - -import java.util.List; - -public record PerformanceUpdateRequest( - 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 totalScheduleCount, - List scheduleList, - List castList, - List staffList -) {} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateResponse.java b/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateResponse.java deleted file mode 100644 index fc2bc6dc..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/PerformanceUpdateResponse.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -import com.beat.domain.performance.domain.BankName; -import com.beat.domain.performance.domain.Genre; - -import java.util.List; - -public record PerformanceUpdateResponse( - 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 PerformanceUpdateResponse 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 PerformanceUpdateResponse(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/update/ScheduleUpdateRequest.java b/src/main/java/com/beat/domain/performance/application/dto/update/ScheduleUpdateRequest.java deleted file mode 100644 index bdaf8728..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/ScheduleUpdateRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -import java.time.LocalDateTime; - -public record ScheduleUpdateRequest( - Long scheduleId, - LocalDateTime performanceDate, - int totalTicketCount, - String scheduleNumber -) {} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/ScheduleUpdateResponse.java b/src/main/java/com/beat/domain/performance/application/dto/update/ScheduleUpdateResponse.java deleted file mode 100644 index 2ef82613..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/ScheduleUpdateResponse.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -import com.beat.domain.schedule.domain.ScheduleNumber; - -import java.time.LocalDateTime; - -public record ScheduleUpdateResponse( - Long scheduleId, - LocalDateTime performanceDate, - int totalTicketCount, - int dueDate, - ScheduleNumber scheduleNumber -) { - public static ScheduleUpdateResponse of(Long scheduleId, LocalDateTime performanceDate, int totalTicketCount, int dueDate, ScheduleNumber scheduleNumber) { - return new ScheduleUpdateResponse(scheduleId, performanceDate, totalTicketCount, dueDate, scheduleNumber); - } -} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateRequest.java b/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateRequest.java deleted file mode 100644 index 00a49735..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateRequest.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -public record StaffUpdateRequest( - Long staffId, - String staffName, - String staffRole, - String staffPhoto -) {} diff --git a/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateResponse.java b/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateResponse.java deleted file mode 100644 index 68bba8d9..00000000 --- a/src/main/java/com/beat/domain/performance/application/dto/update/StaffUpdateResponse.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.beat.domain.performance.application.dto.update; - -public record StaffUpdateResponse( - Long staffId, - String staffName, - String staffRole, - String staffPhoto -) { - public static StaffUpdateResponse of(Long staffId, String staffName, String staffRole, String staffPhoto) { - return new StaffUpdateResponse(staffId, staffName, staffRole, staffPhoto); - } -} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/dao/PerformanceImageRepository.java b/src/main/java/com/beat/domain/performance/dao/PerformanceImageRepository.java new file mode 100644 index 00000000..9f7d74ca --- /dev/null +++ b/src/main/java/com/beat/domain/performance/dao/PerformanceImageRepository.java @@ -0,0 +1,14 @@ +package com.beat.domain.performance.dao; + +import com.beat.domain.performance.domain.PerformanceImage; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; + +public interface PerformanceImageRepository extends JpaRepository { + List findAllByPerformanceId(Long performanceId); + + @Query("SELECT s.id FROM PerformanceImage s WHERE s.performance.id = :performanceId") + List findIdsByPerformanceId(Long performanceId); +} 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 78cc146e..8ba1af31 100644 --- a/src/main/java/com/beat/domain/performance/domain/Performance.java +++ b/src/main/java/com/beat/domain/performance/domain/Performance.java @@ -1,8 +1,10 @@ package com.beat.domain.performance.domain; import com.beat.domain.BaseTimeEntity; +import com.beat.domain.performance.exception.PerformanceErrorCode; import com.beat.domain.promotion.domain.Promotion; import com.beat.domain.user.domain.Users; +import com.beat.global.common.exception.BadRequestException; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Builder; @@ -33,10 +35,10 @@ public class Performance extends BaseTimeEntity { @Column(nullable = false) private int runningTime; - @Column(nullable = false) + @Column(nullable = false, length = 500) private String performanceDescription; - @Column(nullable = false) + @Column(nullable = false, length = 500) private String performanceAttentionNote; @Enumerated(EnumType.STRING) @@ -78,6 +80,9 @@ public class Performance extends BaseTimeEntity { @OnDelete(action = OnDeleteAction.CASCADE) private Users users; + @OneToMany(mappedBy = "performance", cascade = CascadeType.ALL, orphanRemoval = true) + private List performanceImageList = new ArrayList<>(); + @Builder public Performance(String performanceTitle, Genre genre, int runningTime, String performanceDescription, String performanceAttentionNote, BankName bankName, String accountNumber, String accountHolder, String posterImage, String performanceTeamName, String performanceVenue, String performanceContact, @@ -143,4 +148,11 @@ public void update( this.performancePeriod = performancePeriod; this.totalScheduleCount = totalScheduleCount; } + + public void updateTicketPrice(int newTicketPrice) { + if (newTicketPrice < 0) { + throw new BadRequestException(PerformanceErrorCode.NEGATIVE_TICKET_PRICE); + } + this.ticketPrice = newTicketPrice; + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/performance/domain/PerformanceImage.java b/src/main/java/com/beat/domain/performance/domain/PerformanceImage.java new file mode 100644 index 00000000..33bbfb02 --- /dev/null +++ b/src/main/java/com/beat/domain/performance/domain/PerformanceImage.java @@ -0,0 +1,41 @@ +package com.beat.domain.performance.domain; + +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class PerformanceImage { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String performanceImage; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = false) + private Performance performance; + + @Builder + public PerformanceImage(String performanceImage, Performance performance) { + this.performanceImage = performanceImage; + this.performance = performance; + } + + public static PerformanceImage create(String perforemanceImage, Performance performance) { + return PerformanceImage.builder() + .performanceImage(perforemanceImage) + .performance(performance) + .build(); + } + + public void update(String performanceImage) { + this.performanceImage = performanceImage; + } +} 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 5b1ab3ea..8e4f3a0b 100644 --- a/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceErrorCode.java @@ -1,22 +1,30 @@ 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, "해당 공연 정보를 찾을 수 없습니다."), - REQUIRED_DATA_MISSING(400, "필수 데이터가 누락되었습니다."), - INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), - INVALID_REQUEST_FORMAT(400, "잘못된 요청 형식입니다."), - NO_PERFORMANCE_FOUND(404, "공연을 찾을 수 없습니다."), - PERFORMANCE_DELETE_FAILED(403, "예매자가 1명 이상 있을 경우, 공연을 삭제할 수 없습니다."), - NOT_PERFORMANCE_OWNER(403, "해당 공연의 메이커가 아닙니다."), - INTERNAL_SERVER_ERROR(500, "서버 내부 오류입니다.") - ; + PERFORMANCE_NOT_FOUND(404, "해당 공연 정보를 찾을 수 없습니다."), + REQUIRED_DATA_MISSING(400, "필수 데이터가 누락되었습니다."), + INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), + INVALID_REQUEST_FORMAT(400, "잘못된 요청 형식입니다."), + PRICE_UPDATE_NOT_ALLOWED(400, "예매자가 존재하여 가격을 수정할 수 없습니다."), + NEGATIVE_TICKET_PRICE(400, "티켓 가격은 음수일 수 없습니다."), + NO_PERFORMANCE_FOUND(404, "공연을 찾을 수 없습니다."), + PERFORMANCE_DELETE_FAILED(403, "예매자가 1명 이상 있을 경우, 공연을 삭제할 수 없습니다."), + NOT_PERFORMANCE_OWNER(403, "해당 공연의 메이커가 아닙니다."), + MAX_SCHEDULE_LIMIT_EXCEEDED(400, "공연 회차는 최대 10개까지 추가할 수 있습니다."), + INVALID_PERFORMANCE_DESCRIPTION_LENGTH(400, "공연 소개 글자수가 500자를 초과했습니다."), + INVALID_ATTENTION_NOTE_LENGTH(400, "공연 유의사항 글자수가 500자를 초과했습니다."), + INTERNAL_SERVER_ERROR(500, "서버 내부 오류입니다."), + PAST_SCHEDULE_NOT_ALLOWED(400, "과거 날짜 회차를 포함한 공연을 생성할 수 없습니다."), + SCHEDULE_MODIFICATION_NOT_ALLOWED_FOR_ENDED_SCHEDULE(400, "종료된 회차를 수정할 수 없습니다."), + INVALID_TICKET_COUNT(400, "판매된 티켓 수보다 적은 수로 판매할 티켓 매수를 수정할 수 없습니다."); - private final int status; - private final String message; + 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/PerformanceImageErrorCode.java b/src/main/java/com/beat/domain/performance/exception/PerformanceImageErrorCode.java new file mode 100644 index 00000000..f043a71a --- /dev/null +++ b/src/main/java/com/beat/domain/performance/exception/PerformanceImageErrorCode.java @@ -0,0 +1,16 @@ +package com.beat.domain.performance.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum PerformanceImageErrorCode implements BaseErrorCode { + PERFORMANCE_IMAGE_NOT_FOUND(404, "해당 공연 상세이미지를 찾을 수 없습니다."), + PERFORMANCE_IMAGE_NOT_BELONG_TO_PERFORMANCE(403, "해당 싱세이미지는 해당 공연에 속해 있지 않습니다.") + ; + + private final int status; + private final String message; +} diff --git a/src/main/java/com/beat/domain/performance/port/in/PerformanceUseCase.java b/src/main/java/com/beat/domain/performance/port/in/PerformanceUseCase.java new file mode 100644 index 00000000..ea77f90d --- /dev/null +++ b/src/main/java/com/beat/domain/performance/port/in/PerformanceUseCase.java @@ -0,0 +1,7 @@ +package com.beat.domain.performance.port.in; + +import com.beat.domain.performance.domain.Performance; + +public interface PerformanceUseCase { + Performance findById(Long performanceId); +} diff --git a/src/main/java/com/beat/domain/promotion/application/PromotionSchedulerService.java b/src/main/java/com/beat/domain/promotion/application/PromotionSchedulerService.java new file mode 100644 index 00000000..db3e7a24 --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/application/PromotionSchedulerService.java @@ -0,0 +1,44 @@ +package com.beat.domain.promotion.application; + +import com.beat.domain.performance.domain.Performance; +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.schedule.domain.Schedule; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Service +@RequiredArgsConstructor +public class PromotionSchedulerService { + + private final PromotionRepository promotionRepository; + private final ScheduleRepository scheduleRepository; + private final ScheduleService scheduleService; + + @Scheduled(cron = "1 0 0 * * ?") + @Transactional + public void checkAndDeleteInvalidPromotions() { + List promotions = promotionRepository.findAll(); + + for (Promotion promotion : promotions) { + Performance performance = promotion.getPerformance(); + + if (performance == null) { + return; + } + + List schedules = scheduleRepository.findByPerformanceId(performance.getId()); + int minDueDate = scheduleService.getMinDueDate(schedules); + + if (minDueDate < 0) { + promotionRepository.delete(promotion); + } + } + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/promotion/application/PromotionService.java b/src/main/java/com/beat/domain/promotion/application/PromotionService.java new file mode 100644 index 00000000..dd0c8f05 --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/application/PromotionService.java @@ -0,0 +1,57 @@ +package com.beat.domain.promotion.application; + +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionModifyRequest; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.promotion.dao.PromotionRepository; +import com.beat.domain.promotion.domain.CarouselNumber; +import com.beat.domain.promotion.domain.Promotion; +import com.beat.domain.promotion.exception.PromotionErrorCode; +import com.beat.domain.promotion.port.in.PromotionUseCase; +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; + +@Service +@Transactional +@RequiredArgsConstructor +public class PromotionService implements PromotionUseCase { + + private final PromotionRepository promotionRepository; + + @Override + @Transactional(readOnly = true) + public Promotion findById(Long promotionId) { + return promotionRepository.findById(promotionId) + .orElseThrow(() -> new NotFoundException(PromotionErrorCode.PROMOTION_NOT_FOUND)); + } + + @Override + @Transactional(readOnly = true) + public List findAllPromotions() { + return promotionRepository.findAll(); + } + + @Override + public Promotion createPromotion(String newImageUrl, Performance performance, String redirectUrl, + boolean isExternal, CarouselNumber carouselNumber) { + Promotion newPromotion = Promotion.create(newImageUrl, performance, redirectUrl, isExternal, carouselNumber); + return promotionRepository.save(newPromotion); + } + + @Override + public Promotion modifyPromotion(Promotion promotion, Performance performance, PromotionModifyRequest request) { + promotion.updatePromotionDetails(request.carouselNumber(), request.newImageUrl(), request.isExternal(), + request.redirectUrl(), performance); + return promotionRepository.save(promotion); + } + + @Override + public void deletePromotionsByPromotionIds(List promotionIds) { + promotionRepository.deleteByPromotionIds(promotionIds); + } +} diff --git a/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java b/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java index 95a64b0b..077515fc 100644 --- a/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java +++ b/src/main/java/com/beat/domain/promotion/dao/PromotionRepository.java @@ -1,7 +1,24 @@ package com.beat.domain.promotion.dao; +import com.beat.domain.promotion.domain.CarouselNumber; import com.beat.domain.promotion.domain.Promotion; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Modifying; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; public interface PromotionRepository extends JpaRepository { -} + List findAll(); + + @Modifying(clearAutomatically = true) + @Transactional + @Query("DELETE FROM Promotion p WHERE p.id IN :promotionIds") + void deleteByPromotionIds(@Param("promotionIds") List promotionIds); + + @Query("SELECT p FROM Promotion p WHERE p.carouselNumber = :carouselNumber") + Optional findByCarouselNumber(@Param("carouselNumber") CarouselNumber carouselNumber); +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/promotion/domain/CarouselNumber.java b/src/main/java/com/beat/domain/promotion/domain/CarouselNumber.java new file mode 100644 index 00000000..6a8dbe2a --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/domain/CarouselNumber.java @@ -0,0 +1,18 @@ +package com.beat.domain.promotion.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum CarouselNumber { + ONE(1), + TWO(2), + THREE(3), + FOUR(4), + FIVE(5), + SIX(6), + SEVEN(7); + + private final int number; +} \ No newline at end of file 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 445469ef..fa0240a3 100644 --- a/src/main/java/com/beat/domain/promotion/domain/Promotion.java +++ b/src/main/java/com/beat/domain/promotion/domain/Promotion.java @@ -1,7 +1,17 @@ package com.beat.domain.promotion.domain; import com.beat.domain.performance.domain.Performance; -import jakarta.persistence.*; + +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; @@ -12,27 +22,57 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Promotion { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - private Long id; - - @Column(nullable = false) - private String promotionPhoto; - - @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(); - } -} + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String promotionPhoto; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "performance_id", nullable = true) + private Performance performance; + + @Column(nullable = false) + private String redirectUrl; + + @Column(nullable = false) + private boolean isExternal; + + @Enumerated(EnumType.STRING) + @Column(nullable = false) + private CarouselNumber carouselNumber; + + @Builder + public Promotion(String promotionPhoto, Performance performance, String redirectUrl, boolean isExternal, + CarouselNumber carouselNumber) { + this.promotionPhoto = promotionPhoto; + this.performance = performance; + this.redirectUrl = redirectUrl; + this.isExternal = isExternal; + this.carouselNumber = carouselNumber; + } + + public static Promotion create(String promotionPhoto, Performance performance, String redirectUrl, + boolean isExternal, CarouselNumber carouselNumber) { + return Promotion.builder() + .promotionPhoto(promotionPhoto) + .performance(performance) + .redirectUrl(redirectUrl) + .isExternal(isExternal) + .carouselNumber(carouselNumber) + .build(); + } + + /** + * Promotion 정보를 업데이트하는 도메인 메서드 + */ + public void updatePromotionDetails(CarouselNumber carouselNumber, String newImageUrl, boolean isExternal, + String redirectUrl, Performance performance) { + this.carouselNumber = carouselNumber; + this.promotionPhoto = newImageUrl; + this.isExternal = isExternal; + this.redirectUrl = redirectUrl; + this.performance = performance; + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/domain/promotion/exception/PromotionErrorCode.java b/src/main/java/com/beat/domain/promotion/exception/PromotionErrorCode.java new file mode 100644 index 00000000..f7df85e6 --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/exception/PromotionErrorCode.java @@ -0,0 +1,15 @@ +package com.beat.domain.promotion.exception; + +import com.beat.global.common.exception.base.BaseErrorCode; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum PromotionErrorCode implements BaseErrorCode { + PROMOTION_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/promotion/port/in/PromotionUseCase.java b/src/main/java/com/beat/domain/promotion/port/in/PromotionUseCase.java new file mode 100644 index 00000000..de360656 --- /dev/null +++ b/src/main/java/com/beat/domain/promotion/port/in/PromotionUseCase.java @@ -0,0 +1,21 @@ +package com.beat.domain.promotion.port.in; + +import com.beat.admin.application.dto.request.CarouselHandleRequest.PromotionModifyRequest; +import com.beat.domain.performance.domain.Performance; +import com.beat.domain.promotion.domain.CarouselNumber; +import com.beat.domain.promotion.domain.Promotion; + +import java.util.List; + +public interface PromotionUseCase { + Promotion findById(Long promotionId); + + List findAllPromotions(); + + Promotion createPromotion(String newImageUrl, Performance performance, String redirectUrl, boolean isExternal, + CarouselNumber carouselNumber); + + Promotion modifyPromotion(Promotion promotion, Performance performance, PromotionModifyRequest request); + + void deletePromotionsByPromotionIds(List promotionIds); +} \ 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 index a8162ce6..838a17b0 100644 --- a/src/main/java/com/beat/domain/schedule/application/ScheduleService.java +++ b/src/main/java/com/beat/domain/schedule/application/ScheduleService.java @@ -9,10 +9,8 @@ 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.LocalDateTime; import java.time.temporal.ChronoUnit; import java.util.List; import java.util.OptionalInt; @@ -48,36 +46,20 @@ public TicketAvailabilityResponse findTicketAvailability(Long scheduleId, Ticket ); } - private void validateRequest(Long scheduleId, TicketAvailabilityRequest ticketAvailabilityRequest) { - if (ticketAvailabilityRequest.purchaseTicketCount() <= 0 || scheduleId <= 0) { - 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); - } - } - public int calculateDueDate(Schedule schedule) { - // LocalDate 객체를 사용하여 날짜 차이만 계산 int dueDate = (int) ChronoUnit.DAYS.between(LocalDate.now(), schedule.getPerformanceDate().toLocalDate()); return dueDate; } + public int getMinDueDateForPerformance(Long performanceId) { + List schedules = scheduleRepository.findByPerformanceId(performanceId); + return getMinDueDate(schedules); + } + public int getMinDueDate(List schedules) { OptionalInt minPositiveDueDate = schedules.stream() .mapToInt(this::calculateDueDate) @@ -94,4 +76,9 @@ public int getMinDueDate(List schedules) { } } + private void validateRequest(Long scheduleId, TicketAvailabilityRequest ticketAvailabilityRequest) { + if (ticketAvailabilityRequest.purchaseTicketCount() <= 0 || scheduleId <= 0) { + throw new BadRequestException(ScheduleErrorCode.INVALID_DATA_FORMAT); + } + } } 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 a67b18b6..3a471b8e 100644 --- a/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java +++ b/src/main/java/com/beat/domain/schedule/dao/ScheduleRepository.java @@ -21,4 +21,11 @@ public interface ScheduleRepository extends JpaRepository { List findAllByPerformanceId(Long performanceId); @Query("SELECT s.id FROM Schedule s WHERE s.performance.id = :performanceId") - List findIdsByPerformanceId(@Param("performanceId") Long performanceId);} \ No newline at end of file + List findIdsByPerformanceId(@Param("performanceId") Long performanceId); + + int countByPerformanceId(Long performanceId); + + // 기존의 PENDING 상태인 스케줄들을 조회하는 메소드 + @Query("SELECT s FROM Schedule s WHERE s.isBooking = true") + List findPendingSchedules(); +} \ 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 d3e33df4..f9a8e05f 100644 --- a/src/main/java/com/beat/domain/schedule/domain/Schedule.java +++ b/src/main/java/com/beat/domain/schedule/domain/Schedule.java @@ -89,4 +89,11 @@ public void decreaseSoldTicketCount(int count) { } } + public void updateScheduleNumber(ScheduleNumber scheduleNumber) { + this.scheduleNumber = scheduleNumber; + } + + public void updateIsBooking(boolean isBooking) { + this.isBooking = isBooking; + } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/schedule/domain/ScheduleNumber.java b/src/main/java/com/beat/domain/schedule/domain/ScheduleNumber.java index 89d3dc73..60b93aa6 100644 --- a/src/main/java/com/beat/domain/schedule/domain/ScheduleNumber.java +++ b/src/main/java/com/beat/domain/schedule/domain/ScheduleNumber.java @@ -8,7 +8,14 @@ public enum ScheduleNumber { FIRST("1회차"), SECOND("2회차"), - THIRD("3회차"); + THIRD("3회차"), + FOURTH("4회차"), + FIFTH("5회차"), + SIXTH("6회차"), + SEVENTH("7회차"), + EIGHTH("8회차"), + NINTH("9회차"), + TENTH("10회차"); private final String displayName; } diff --git a/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java b/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java index e9af170a..98e02ad1 100644 --- a/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java +++ b/src/main/java/com/beat/domain/schedule/exception/ScheduleErrorCode.java @@ -8,6 +8,7 @@ @RequiredArgsConstructor public enum ScheduleErrorCode implements BaseErrorCode { INVALID_DATA_FORMAT(400, "잘못된 데이터 형식입니다."), + SCHEDULE_NOT_BELONG_TO_PERFORMANCE(403,"해당 스케줄은 해당 공연에 속해 있지 않습니다."), NO_SCHEDULE_FOUND(404, "해당 회차를 찾을 수 없습니다."), INSUFFICIENT_TICKETS(409, "요청한 티켓 수량이 잔여 티켓 수를 초과했습니다. 다른 수량을 선택해 주세요."), EXCESS_TICKET_DELETE(409, "예매된 티켓 수 이상을 삭제할 수 없습니다.") diff --git a/src/main/java/com/beat/domain/staff/dao/StaffRepository.java b/src/main/java/com/beat/domain/staff/dao/StaffRepository.java index d3af9518..f32801ea 100644 --- a/src/main/java/com/beat/domain/staff/dao/StaffRepository.java +++ b/src/main/java/com/beat/domain/staff/dao/StaffRepository.java @@ -2,6 +2,7 @@ import com.beat.domain.staff.domain.Staff; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import java.util.List; @@ -9,4 +10,7 @@ public interface StaffRepository extends JpaRepository { List findByPerformanceId(Long performanceId); List findAllByPerformanceId(Long performanceId); + + @Query("SELECT s.id FROM Staff s WHERE s.performance.id = :performanceId") + List findIdsByPerformanceId(Long performanceId); } diff --git a/src/main/java/com/beat/domain/staff/exception/StaffErrorCode.java b/src/main/java/com/beat/domain/staff/exception/StaffErrorCode.java index 706e50d8..495ee148 100644 --- a/src/main/java/com/beat/domain/staff/exception/StaffErrorCode.java +++ b/src/main/java/com/beat/domain/staff/exception/StaffErrorCode.java @@ -7,6 +7,7 @@ @Getter @RequiredArgsConstructor public enum StaffErrorCode implements BaseErrorCode { + STAFF_NOT_BELONG_TO_PERFORMANCE(403, "해당 스태프는 해당 공연에 속해있지 않습니다."), STAFF_NOT_FOUND(404, "스태프가 존재하지 않습니다.") ; diff --git a/src/main/java/com/beat/domain/user/application/UserService.java b/src/main/java/com/beat/domain/user/application/UserService.java index 5c8a1ace..f485c0fc 100644 --- a/src/main/java/com/beat/domain/user/application/UserService.java +++ b/src/main/java/com/beat/domain/user/application/UserService.java @@ -1,4 +1,33 @@ package com.beat.domain.user.application; -public class UserService { -} +import com.beat.domain.user.dao.UserRepository; +import com.beat.domain.user.domain.Users; +import com.beat.domain.user.exception.UserErrorCode; +import com.beat.domain.user.port.in.UserUseCase; +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; + +@Service +@RequiredArgsConstructor +public class UserService implements UserUseCase { + private final UserRepository userRepository; + + @Override + @Transactional(readOnly = true) + public List findAllUsers() { + return userRepository.findAll(); + } + + @Override + @Transactional(readOnly = true) + public Users findUserByUserId(final Long userId) { + return userRepository.findById(userId) + .orElseThrow(() -> new NotFoundException(UserErrorCode.USER_NOT_FOUND)); + } +} \ No newline at end of file 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 deleted file mode 100644 index 61df11c4..00000000 --- a/src/main/java/com/beat/domain/user/application/dto/UserResponse.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.beat.domain.user.application.dto; - -public record UserResponse( - -) { - -} diff --git a/src/main/java/com/beat/domain/user/domain/Role.java b/src/main/java/com/beat/domain/user/domain/Role.java new file mode 100644 index 00000000..1854d454 --- /dev/null +++ b/src/main/java/com/beat/domain/user/domain/Role.java @@ -0,0 +1,33 @@ +package com.beat.domain.user.domain; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; + +@RequiredArgsConstructor +@Getter +public enum Role { + + USER("ROLE_USER"), + MEMBER("ROLE_MEMBER"), + ADMIN("ROLE_ADMIN"); + + private final String roleName; + + /** + * GrantedAuthority로 변환하는 메서드. + * Spring Security에서 사용자 권한을 처리할 때 사용. + */ + public GrantedAuthority toGrantedAuthority() { + return new SimpleGrantedAuthority(roleName); + } + + /** + * 역할 이름을 반환하는 메서드. + * 예: "ROLE_USER", "ROLE_MEMBER", "ROLE_ADMIN". + */ + public String getRoleName() { + return this.roleName; + } +} \ No newline at end of file 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 b2fc9059..6b017cba 100644 --- a/src/main/java/com/beat/domain/user/domain/Users.java +++ b/src/main/java/com/beat/domain/user/domain/Users.java @@ -1,28 +1,46 @@ package com.beat.domain.user.domain; +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 lombok.AccessLevel; import lombok.Builder; import lombok.Getter; +import lombok.NoArgsConstructor; import lombok.Setter; @Entity @Getter @Setter +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Users { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; + @Enumerated(EnumType.STRING) + @Column(nullable = false, columnDefinition = "varchar(10) default 'USER'") + private Role role; + @Builder - public Users() { + public Users(Role role) { + this.role = role; } public static Users create() { return Users.builder() + .role(Role.USER) + .build(); + } + + public static Users createWithRole(Role role) { + return Users.builder() + .role(role) .build(); } } \ No newline at end of file diff --git a/src/main/java/com/beat/domain/user/port/in/UserUseCase.java b/src/main/java/com/beat/domain/user/port/in/UserUseCase.java new file mode 100644 index 00000000..4ef38873 --- /dev/null +++ b/src/main/java/com/beat/domain/user/port/in/UserUseCase.java @@ -0,0 +1,11 @@ +package com.beat.domain.user.port.in; + +import com.beat.domain.user.domain.Users; + +import java.util.List; + +public interface UserUseCase { + List findAllUsers(); + + Users findUserByUserId(final Long userId); +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/annotation/CurrentMember.java b/src/main/java/com/beat/global/auth/annotation/CurrentMember.java index 8651ab1c..d3327b83 100644 --- a/src/main/java/com/beat/global/auth/annotation/CurrentMember.java +++ b/src/main/java/com/beat/global/auth/annotation/CurrentMember.java @@ -6,8 +6,11 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import io.swagger.v3.oas.annotations.Parameter; + @Target(ElementType.PARAMETER) @Retention(RetentionPolicy.RUNTIME) @Documented +@Parameter(hidden = true) public @interface CurrentMember { } \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java b/src/main/java/com/beat/global/auth/client/application/KakaoSocialService.java similarity index 96% rename from src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java rename to src/main/java/com/beat/global/auth/client/application/KakaoSocialService.java index 36471454..367e077a 100644 --- a/src/main/java/com/beat/global/auth/client/service/KakaoSocialService.java +++ b/src/main/java/com/beat/global/auth/client/application/KakaoSocialService.java @@ -1,4 +1,4 @@ -package com.beat.global.auth.client.service; +package com.beat.global.auth.client.application; import com.beat.domain.member.domain.SocialType; import com.beat.global.auth.client.dto.MemberInfoResponse; @@ -24,10 +24,11 @@ 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; + private String redirectUri; @Value("${spring.security.oauth2.client.registration.kakao.client-id}") private String clientId; + private final KakaoApiClient kakaoApiClient; private final KakaoAuthApiClient kakaoAuthApiClient; @@ -55,7 +56,7 @@ private String getOAuth2Authentication( KakaoAccessTokenResponse response = kakaoAuthApiClient.getOAuth2AccessToken( AUTH_CODE, clientId, - REDIRECT_URI, + redirectUri, authorizationCode ); log.info("Received OAuth2 authentication response: {}", response); diff --git a/src/main/java/com/beat/global/auth/client/service/SocialService.java b/src/main/java/com/beat/global/auth/client/application/SocialService.java similarity index 83% rename from src/main/java/com/beat/global/auth/client/service/SocialService.java rename to src/main/java/com/beat/global/auth/client/application/SocialService.java index 3934d2ef..5900d7cc 100644 --- a/src/main/java/com/beat/global/auth/client/service/SocialService.java +++ b/src/main/java/com/beat/global/auth/client/application/SocialService.java @@ -1,4 +1,4 @@ -package com.beat.global.auth.client.service; +package com.beat.global.auth.client.application; import com.beat.global.auth.client.dto.MemberInfoResponse; import com.beat.global.auth.client.dto.MemberLoginRequest; 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 index 6097bfeb..ba41fd5c 100644 --- a/src/main/java/com/beat/global/auth/jwt/application/TokenService.java +++ b/src/main/java/com/beat/global/auth/jwt/application/TokenService.java @@ -6,8 +6,10 @@ import com.beat.global.common.exception.NotFoundException; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +@Slf4j @RequiredArgsConstructor @Service public class TokenService { @@ -16,25 +18,22 @@ public class TokenService { @Transactional public void saveRefreshToken(final Long memberId, final String refreshToken) { - tokenRepository.save( - Token.of(memberId, 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) - ); + .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) - ); + .orElseThrow(() -> new NotFoundException(TokenErrorCode.REFRESH_TOKEN_NOT_FOUND)); + tokenRepository.delete(token); + log.info("Deleted refresh token: {}", token); } } \ No newline at end of file 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 index c8ab889a..87a87eb0 100644 --- a/src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java +++ b/src/main/java/com/beat/global/auth/jwt/exception/TokenErrorCode.java @@ -8,9 +8,16 @@ @RequiredArgsConstructor public enum TokenErrorCode implements BaseErrorCode { - AUTHENTICATION_CODE_EXPIRED(403, "토큰이 만료되었습니다"), - REFRESH_TOKEN_NOT_FOUND(404, "리프레쉬토큰이 없습니다"), - TOKEN_INCORRECT_ERROR(400, "잘못된 토큰입니다"); + AUTHENTICATION_CODE_EXPIRED(401, "인가코드가 만료되었습니다"), + REFRESH_TOKEN_NOT_FOUND(404, "리프레쉬 토큰이 존재하지 않습니다"), + INVALID_REFRESH_TOKEN_ERROR(400, "잘못된 리프레쉬 토큰입니다"), + REFRESH_TOKEN_MEMBER_ID_MISMATCH_ERROR(400, "리프레쉬 토큰의 사용자 정보가 일치하지 않습니다"), + REFRESH_TOKEN_EXPIRED_ERROR(401, "리프레쉬 토큰이 만료되었습니다"), + REFRESH_TOKEN_SIGNATURE_ERROR(400, "리프레쉬 토큰의 서명의 잘못 되었습니다"), + UNSUPPORTED_REFRESH_TOKEN_ERROR(400, "지원하지 않는 리프레쉬 토큰입니다"), + REFRESH_TOKEN_EMPTY_ERROR(400, "리프레쉬 토큰이 비어있습니다"), + UNKNOWN_REFRESH_TOKEN_ERROR(500, "알 수 없는 리프레쉬 토큰 오류가 발생했습니다") + ; private final int status; private final String message; 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 7e3365e3..9177cca6 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 @@ -1,56 +1,112 @@ package com.beat.global.auth.jwt.filter; - +import com.beat.domain.user.domain.Role; import com.beat.global.auth.jwt.provider.JwtTokenProvider; +import com.beat.global.auth.jwt.provider.JwtValidationType; +import com.beat.global.auth.security.AdminAuthentication; 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 java.util.Collection; +import java.util.List; + import lombok.NonNull; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; 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 (StringUtils.hasText(token) && 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 e) { - 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 ")) { - return bearerToken.substring("Bearer ".length()); - } - return null; - } + private final JwtTokenProvider jwtTokenProvider; + + @Override + protected void doFilterInternal(@NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain) throws ServletException, IOException { + + final String token = getJwtFromRequest(request); + + if (!StringUtils.hasText(token)) { + log.info("JWT Token not found in request header. Assuming guest access or public API request."); + filterChain.doFilter(request, response); + return; + } + + try { + JwtValidationType validationType = jwtTokenProvider.validateToken(token); + + if (validationType == JwtValidationType.VALID_JWT) { + setAuthentication(token, request); + filterChain.doFilter(request, response); + } else { + handleInvalidToken(validationType, response); + } + } catch (Exception e) { + log.error("JWT Authentication Exception: ", e); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); // 500 응답 + } + } + + private void setAuthentication(String token, HttpServletRequest request) { + Long memberId = jwtTokenProvider.getMemberIdFromJwt(token); + Role role = jwtTokenProvider.getRoleFromJwt(token); + + log.info("Setting authentication for memberId: {} with role: {}", memberId, role); + + Collection authorities = List.of(role.toGrantedAuthority()); + UsernamePasswordAuthenticationToken authentication = createAuthentication(memberId, authorities, role); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + + log.info("Authentication set: memberId: {}, role: {}", memberId, role); + + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void handleInvalidToken(JwtValidationType validationType, HttpServletResponse response) { + if (validationType == JwtValidationType.EXPIRED_JWT_TOKEN) { + log.warn("JWT Token is expired"); + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 응답 + } else { + log.warn("JWT Token is invalid"); + response.setStatus(HttpServletResponse.SC_BAD_REQUEST); // 400 응답 + } + } + + private UsernamePasswordAuthenticationToken createAuthentication(Long memberId, + Collection authorities, Role role) { + log.info("Creating authentication for memberId: {} with role: {}", memberId, role); + + if (role == Role.ADMIN) { + log.info("Creating AdminAuthentication for memberId: {}", memberId); + return new AdminAuthentication(memberId.toString(), null, authorities); + } else if (role == Role.MEMBER) { + log.info("Creating MemberAuthentication for memberId: {}", memberId); + return new MemberAuthentication(memberId.toString(), null, authorities); + } + log.error("Unknown role: {}", role); + throw new IllegalArgumentException("Unknown role: " + role); + } + + 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 index 1aac0d78..1ac5d6ea 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 @@ -1,5 +1,7 @@ package com.beat.global.auth.jwt.provider; +import com.beat.domain.user.domain.Role; + import io.jsonwebtoken.Claims; import io.jsonwebtoken.ExpiredJwtException; import io.jsonwebtoken.Header; @@ -7,86 +9,129 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.UnsupportedJwtException; import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; import jakarta.annotation.PostConstruct; + import java.nio.charset.StandardCharsets; import java.util.Base64; import java.util.Date; + import javax.crypto.SecretKey; + +import lombok.extern.slf4j.Slf4j; + import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; +@Slf4j @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()); //SecretKey 통해 서명 생성 - return Keys.hmacShaKeyFor(encodedKey.getBytes()); //일반적으로 HMAC (Hash-based Message Authentication Code) 알고리즘 사용 - } - - public JwtValidationType validateToken(String token) { - try { - 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()); - } + @Value("${jwt.secret}") + private String jwtSecret; + + @Value("${jwt.access-token-expire-time}") + private long accessTokenExpireTime; + + @Value("${jwt.refresh-token-expire-time}") + private long refreshTokenExpireTime; + + private static final String MEMBER_ID = "memberId"; + private static final String ROLE_KEY = "role"; + + @PostConstruct + protected void init() { + jwtSecret = Base64.getEncoder().encodeToString(jwtSecret.getBytes(StandardCharsets.UTF_8)); + } + + public String issueAccessToken(final Authentication authentication) { + return issueToken(authentication, accessTokenExpireTime); + } + + public String issueRefreshToken(final Authentication authentication) { + return issueToken(authentication, refreshTokenExpireTime); + } + + 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()); + log.info("Added member ID to claims: {}", authentication.getPrincipal()); + log.info("Authorities before token generation: {}", authentication.getAuthorities()); + + String role = authentication.getAuthorities() + .stream() + .map(GrantedAuthority::getAuthority) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("No authorities found for user")); + + log.info("Selected role for token: {}", role); + + claims.put(ROLE_KEY, role); + log.info("Added role to claims: {}", role); + + return Jwts.builder() + .setHeaderParam(Header.TYPE, Header.JWT_TYPE) + .setClaims(claims) + .signWith(getSigningKey()) + .compact(); + } + + private SecretKey getSigningKey() { + String encodedKey = Base64.getEncoder().encodeToString(jwtSecret.getBytes()); + return Keys.hmacShaKeyFor(encodedKey.getBytes()); + } + + public JwtValidationType validateToken(String token) { + try { + Claims claims = getBody(token); + return JwtValidationType.VALID_JWT; + } catch (MalformedJwtException ex) { + log.error("Invalid JWT Token: {}", ex.getMessage()); + return JwtValidationType.INVALID_JWT_TOKEN; + } catch (ExpiredJwtException ex) { + log.error("Expired JWT Token: {}", ex.getMessage()); + return JwtValidationType.EXPIRED_JWT_TOKEN; + } catch (UnsupportedJwtException ex) { + log.error("Unsupported JWT Token: {}", ex.getMessage()); + return JwtValidationType.UNSUPPORTED_JWT_TOKEN; + } catch (IllegalArgumentException ex) { + log.error("Empty JWT Token or Illegal Argument: {}", ex.getMessage()); + return JwtValidationType.EMPTY_JWT; + } catch (SignatureException ex) { + log.error("Invalid JWT Signature: {}", ex.getMessage()); + return JwtValidationType.INVALID_JWT_SIGNATURE; + } + } + + private Claims getBody(final String token) { + return Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token).getBody(); + } + + public Long getMemberIdFromJwt(String token) { + Claims claims = getBody(token); + Long memberId = Long.valueOf(claims.get(MEMBER_ID).toString()); + + // 로그 추가: memberId 확인 + log.info("Extracted memberId from JWT: {}", memberId); + + return memberId; + } + + public Role getRoleFromJwt(String token) { + Claims claims = getBody(token); + String roleName = claims.get(ROLE_KEY, String.class); + + log.info("Extracted role from JWT: {}", roleName); + + // "ROLE_" 접두사 제거 + String enumValue = roleName.replace("ROLE_", ""); + log.info("Final role after processing: {}", enumValue); + + return Role.valueOf(enumValue.toUpperCase()); + } } \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/redis/Token.java b/src/main/java/com/beat/global/auth/redis/Token.java index 6eab8fce..69bed8e7 100644 --- a/src/main/java/com/beat/global/auth/redis/Token.java +++ b/src/main/java/com/beat/global/auth/redis/Token.java @@ -7,7 +7,7 @@ import org.springframework.data.redis.core.RedisHash; import org.springframework.data.redis.core.index.Indexed; -@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 1000L * 14) +@RedisHash(value = "refreshToken", timeToLive = 1209600) @Getter @Builder public class Token { diff --git a/src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java b/src/main/java/com/beat/global/auth/resolver/CurrentMemberArgumentResolver.java similarity index 94% rename from src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java rename to src/main/java/com/beat/global/auth/resolver/CurrentMemberArgumentResolver.java index e14e4f37..2290cc94 100644 --- a/src/main/java/com/beat/global/auth/resolver/CurrentUserArgumentResolver.java +++ b/src/main/java/com/beat/global/auth/resolver/CurrentMemberArgumentResolver.java @@ -13,7 +13,7 @@ @Component @RequiredArgsConstructor -public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { +public class CurrentMemberArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { diff --git a/src/main/java/com/beat/global/auth/security/AdminAuthentication.java b/src/main/java/com/beat/global/auth/security/AdminAuthentication.java new file mode 100644 index 00000000..d1efbfae --- /dev/null +++ b/src/main/java/com/beat/global/auth/security/AdminAuthentication.java @@ -0,0 +1,18 @@ +package com.beat.global.auth.security; + +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.GrantedAuthority; + +import java.util.Collection; + +public class AdminAuthentication extends UsernamePasswordAuthenticationToken { + + public AdminAuthentication(Object principal, Object credentials, + Collection authorities) { + super(principal, credentials, authorities); + } + + public Long getAdminId() { + return (Long) getPrincipal(); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java b/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java index 65b67724..6855ac4d 100644 --- a/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java +++ b/src/main/java/com/beat/global/auth/security/CustomAccessDeniedHandler.java @@ -4,15 +4,21 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; +import lombok.extern.slf4j.Slf4j; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; +@Slf4j @Component public class CustomAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { + String path = request.getRequestURI(); + String method = request.getMethod(); + log.warn("Access Denied: Method: {}, Path: {}, Message: {}", method, path, accessDeniedException.getMessage()); + setResponse(response); } diff --git a/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java b/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java index 55f6ef33..2591e943 100644 --- a/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java +++ b/src/main/java/com/beat/global/auth/security/CustomJwtAuthenticationEntryPoint.java @@ -3,20 +3,24 @@ import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; - +import lombok.extern.slf4j.Slf4j; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +@Slf4j @Component public class CustomJwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) { + String path = request.getRequestURI(); + String method = request.getMethod(); + log.warn("Unauthorized access attempt: Method: {}, Path: {}, Message: {}", method, path, authException.getMessage()); + setResponse(response); } private void setResponse(HttpServletResponse response) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); } - -} +} \ 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 dd99398e..7ab26c3d 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.domain.user.domain.Role; import com.beat.global.auth.jwt.filter.JwtAuthenticationFilter; import com.beat.global.auth.security.CustomAccessDeniedHandler; import com.beat.global.auth.security.CustomJwtAuthenticationEntryPoint; @@ -8,7 +9,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; @@ -22,6 +22,7 @@ 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", @@ -36,7 +37,12 @@ public class SecurityConfig { "/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", - "/api/files/**" + "/api/files/**", + "/error" + }; + + private static final String[] AUTH_ADMIN_ONLY = { + "/api/admin/**" }; @Bean @@ -44,19 +50,16 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti http.csrf(AbstractHttpConfigurer::disable) .formLogin(AbstractHttpConfigurer::disable) .httpBasic(AbstractHttpConfigurer::disable) - .sessionManagement(session -> { - session.sessionCreationPolicy(SessionCreationPolicy.STATELESS); - }) + .sessionManagement(session -> + session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .exceptionHandling(exception -> - { - exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint); - exception.accessDeniedHandler(customAccessDeniedHandler); - }); + exception.authenticationEntryPoint(customJwtAuthenticationEntryPoint) + .accessDeniedHandler(customAccessDeniedHandler)); - http.authorizeHttpRequests(auth -> { - auth.requestMatchers(AUTH_WHITELIST).permitAll(); - auth.anyRequest().authenticated(); - }) + http.authorizeHttpRequests(auth -> + auth.requestMatchers(AUTH_WHITELIST).permitAll() + .requestMatchers(AUTH_ADMIN_ONLY).hasAuthority(Role.ADMIN.getRoleName()) + .anyRequest().authenticated()) .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); 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 ce53c953..341035ab 100644 --- a/src/main/java/com/beat/global/common/config/WebConfig.java +++ b/src/main/java/com/beat/global/common/config/WebConfig.java @@ -1,7 +1,10 @@ package com.beat.global.common.config; -import com.beat.global.auth.resolver.CurrentUserArgumentResolver; +import com.beat.global.auth.resolver.CurrentMemberArgumentResolver; + import lombok.RequiredArgsConstructor; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Configuration; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; @@ -13,19 +16,22 @@ @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("/**") - .allowedOriginPatterns("*") // 모든 도메인 허용 - .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") - .allowedHeaders("*") - .allowCredentials(true); - } + private final CurrentMemberArgumentResolver currentMemberArgumentResolver; + + @Value("${cors.allowed-origins}") + private String[] allowedOrigins; + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(currentMemberArgumentResolver); + } + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedOrigins(allowedOrigins) + .allowedMethods("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS") + .allowedHeaders("*") + .allowCredentials(true); + } } \ 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 ccc83c7d..7a7db761 100644 --- a/src/main/java/com/beat/global/common/dto/SuccessResponse.java +++ b/src/main/java/com/beat/global/common/dto/SuccessResponse.java @@ -7,11 +7,11 @@ public record SuccessResponse( String message, T data ) { - public static SuccessResponse of(final BaseSuccessCode baseSuccessCode, final T data) { - return new SuccessResponse(baseSuccessCode.getStatus(), baseSuccessCode.getMessage(), data); + public static SuccessResponse of(final BaseSuccessCode baseSuccessCode, final T data) { + return new SuccessResponse<>(baseSuccessCode.getStatus(), baseSuccessCode.getMessage(), data); } - public static SuccessResponse from(final BaseSuccessCode baseSuccessCode) { - return new SuccessResponse(baseSuccessCode.getStatus(), baseSuccessCode.getMessage(), null); + 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 34c38c70..d234b07c 100644 --- a/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/beat/global/common/handler/GlobalExceptionHandler.java @@ -2,53 +2,96 @@ import com.beat.global.common.dto.ErrorResponse; import com.beat.global.common.exception.BadRequestException; +import com.beat.global.common.exception.BeatException; 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; +import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; -import java.util.Objects; +import java.util.Optional; +@Slf4j @RestControllerAdvice public class GlobalExceptionHandler { + /** + * 400 BAD_REQUEST + */ @ExceptionHandler(BadRequestException.class) public ResponseEntity handleBadRequestException(final BadRequestException e) { + log.error("BadRequestException: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.from(e.getBaseErrorCode())); } + + @ExceptionHandler(MethodArgumentNotValidException.class) + protected ResponseEntity handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + String errorMessage = Optional.ofNullable(e.getBindingResult().getFieldError()) + .map(FieldError::getDefaultMessage) // 메서드 참조 사용 + .orElse("Validation error"); + + log.warn("MethodArgumentNotValidException: {}", errorMessage); + return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(ErrorResponse.of(HttpStatus.BAD_REQUEST.value(), errorMessage)); + } + + /** + * 401 UNAUTHORIZED + */ @ExceptionHandler(UnauthorizedException.class) public ResponseEntity handleUnauthorizedException(final UnauthorizedException e) { + log.error("UnauthorizedException: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(ErrorResponse.from(e.getBaseErrorCode())); } + /** + * 403 FORBIDDEN + */ @ExceptionHandler(ForbiddenException.class) public ResponseEntity handleForbiddenException(final ForbiddenException e) { + log.error("ForbiddenException: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(ErrorResponse.from(e.getBaseErrorCode())); } + /** + * 404 NOT_FOUND + */ @ExceptionHandler(NotFoundException.class) protected ResponseEntity handleNotFoundException(final NotFoundException e) { + log.error("NotFoundException: {}", e.getMessage()); 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())); - } - + /** + * 409 CONFLICT + */ @ExceptionHandler(ConflictException.class) protected ResponseEntity handleConflictException(final ConflictException e) { + log.error("ConflictException: {}", e.getMessage()); return ResponseEntity.status(HttpStatus.CONFLICT).body(ErrorResponse.from(e.getBaseErrorCode())); } + /** + * 500 INTERNEL_SERVER + */ @ExceptionHandler(Exception.class) protected ResponseEntity handleException(final Exception e) { + log.error("Unexpected server error: ", e); return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(ErrorResponse.of(HttpStatus.INTERNAL_SERVER_ERROR.value(), "서버 내부 오류입니다.")); } -} + + /** + * CUSTOM_ERROR + */ + @ExceptionHandler(BeatException.class) + public ResponseEntity handleBeatException(final BeatException e) { + log.error("BeatException occurred: ", e); + return ResponseEntity.status(e.getBaseErrorCode().getStatus()).body(ErrorResponse.from(e.getBaseErrorCode())); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/common/scheduler/application/JobSchedulerService.java b/src/main/java/com/beat/global/common/scheduler/application/JobSchedulerService.java new file mode 100644 index 00000000..622c45c4 --- /dev/null +++ b/src/main/java/com/beat/global/common/scheduler/application/JobSchedulerService.java @@ -0,0 +1,120 @@ +package com.beat.global.common.scheduler.application; + +import com.beat.domain.schedule.dao.ScheduleRepository; +import com.beat.domain.schedule.domain.Schedule; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.hibernate.Hibernate; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.scheduling.TaskScheduler; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ScheduledFuture; + +@Slf4j +@Service +@RequiredArgsConstructor +public class JobSchedulerService { + + private final ScheduleRepository scheduleRepository; + private final TaskScheduler taskScheduler; + + // 스케줄 ID와 관련된 작업을 관리하기 위한 ConcurrentHashMap 선언 + private final Map> scheduledTasks = new ConcurrentHashMap<>(); + + // ApplicationReadyEvent는 애플리케이션이 완전히 시작된 후에 한 번만 실행됩니다. + @EventListener(ApplicationReadyEvent.class) + @Transactional + public void onApplicationReady(ApplicationReadyEvent event) { + // 서버 시작 시점에만 실행되도록 트랜잭션 관리가 필요한 부분을 다른 메서드로 분리 + log.info("onApplicationReady() method triggered."); + schedulePendingPerformances(); + } + + @Transactional(readOnly = true) + public void schedulePendingPerformances() { + // PENDING 상태의 스케줄들을 조회하여 트랜잭션 내에서 처리 + List schedules = scheduleRepository.findPendingSchedules(); + + // 각각의 스케줄에 대해 지연 로딩 처리 및 스케줄링 작업 수행 + schedules.forEach(schedule -> { + // Performance 초기화 시점은 트랜잭션 경계 내에서 이루어져야 함 + Hibernate.initialize(schedule.getPerformance()); + addScheduleIfNotExists(schedule); + }); + } + + // 스케줄 종료 시점에 맞춰 isBooking 업데이트 + @Transactional + public void addScheduleIfNotExists(Schedule schedule) { + if (scheduledTasks.containsKey(schedule.getId())) { + log.debug("Schedule ID {} is already scheduled. Skipping duplicate registration.", schedule.getId()); + return; + } + + // 여기서 데이터베이스 X-Lock을 걸어 중복 실행 방지 + scheduleRepository.lockById(schedule.getId()) + .ifPresentOrElse( + lockedSchedule -> { + log.info("Lock acquired for Schedule ID: {}", lockedSchedule.getId()); + LocalDateTime performanceEndTime = lockedSchedule.getPerformanceDate() + .plusMinutes(lockedSchedule.getPerformance().getRunningTime()); + + log.info("Scheduling task for Schedule ID: {} at {}", lockedSchedule.getId(), performanceEndTime); + + ScheduledFuture scheduledTask = taskScheduler.schedule( + () -> updateIsBookingFalse(lockedSchedule.getId()), + Date.from(performanceEndTime.atZone(ZoneId.systemDefault()).toInstant()) + ); + + scheduledTasks.put(lockedSchedule.getId(), scheduledTask); + log.debug("Task added for Schedule ID: {}", lockedSchedule.getId()); + + logScheduledTasks(); + }, + () -> log.warn("Failed to acquire lock for Schedule ID: {}", schedule.getId()) + ); + } + + // 스케줄 종료 시 isBooking을 false로 업데이트 + @Transactional + public void updateIsBookingFalse(Long scheduleId) { + Schedule schedule = scheduleRepository.findById(scheduleId) + .orElseThrow(() -> new IllegalStateException("Schedule not found: " + scheduleId)); + + log.info("Updating isBooking to false for schedule ID: {}", scheduleId); + schedule.updateIsBooking(false); + scheduleRepository.save(schedule); + + // 스케줄 작업 완료 후 Map에서 삭제 + scheduledTasks.remove(scheduleId); + log.debug("Completed Task removed for Schedule ID: {}", scheduleId); + logScheduledTasks(); + } + + public void cancelScheduledTaskForPerformance(Schedule schedule) { + ScheduledFuture scheduledTask = scheduledTasks.get(schedule.getId()); + if (scheduledTask != null && !scheduledTask.isDone()) { + scheduledTask.cancel(true); // 작업이 완료되지 않았다면 취소 + scheduledTasks.remove(schedule.getId()); + log.info("Cancelled Task removed for Schedule ID: {}", schedule.getId()); + logScheduledTasks(); + } + } + + // 현재 등록된 스케줄 로그 출력 + public void logScheduledTasks() { + scheduledTasks.forEach((scheduleId, future) -> { + log.debug("Scheduled task for Schedule ID: {} is currently {}.", + scheduleId, (future.isCancelled() ? "Cancelled" : "Scheduled")); + }); + } +} diff --git a/src/main/java/com/beat/global/external/s3/api/FileApi.java b/src/main/java/com/beat/global/external/s3/api/FileApi.java new file mode 100644 index 00000000..0557f2dc --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/api/FileApi.java @@ -0,0 +1,41 @@ +package com.beat.global.external.s3.api; + +import com.beat.global.common.dto.ErrorResponse; +import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.external.s3.application.dto.PerformanceMakerPresignedUrlFindAllResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.List; + +@Tag(name = "Image - Performance PreSigned Url", description = "Performance PreSigned Url 발급 API") +public interface FileApi { + + @Operation(summary = "공연 이미지 업로드 Presigned URL 발급", description = "공연 등록 시 업로드할 이미지에 대한 presigned URL을 발급 받는 GET API") + @ApiResponses( + value = { + @ApiResponse( + responseCode = "200", + description = "공연 메이커를 위한 Presigned URL 발급 성공.", + content = @Content(schema = @Schema(implementation = SuccessResponse.class)) + ), + @ApiResponse( + responseCode = "500", + description = "S3 PreSigned url을 받아오기에 실패했습니다.", + content = @Content(schema = @Schema(implementation = ErrorResponse.class)) + ) + } + ) + ResponseEntity> generateAllPresignedUrls( + @RequestParam String posterImage, + @RequestParam(required = false) List castImages, + @RequestParam(required = false) List staffImages, + @RequestParam(required = false) List performanceImages + ); +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/api/FileController.java b/src/main/java/com/beat/global/external/s3/api/FileController.java new file mode 100644 index 00000000..5c72455e --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/api/FileController.java @@ -0,0 +1,44 @@ +package com.beat.global.external.s3.api; + +import com.beat.global.common.dto.SuccessResponse; +import com.beat.global.external.s3.exception.FileSuccessCode; +import com.beat.global.external.s3.application.dto.PerformanceMakerPresignedUrlFindAllResponse; +import com.beat.global.external.s3.port.in.FileUseCase; +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; + +import java.util.List; + +@RestController +@RequestMapping("/api/files") +@RequiredArgsConstructor +public class FileController implements FileApi { + + private final FileUseCase fileUseCase; + + @GetMapping("/presigned-url") + @Override + public ResponseEntity> generateAllPresignedUrls( + @RequestParam String posterImage, + @RequestParam(required = false) List castImages, + @RequestParam(required = false) List staffImages, + @RequestParam(required = false) List performanceImages) { + // 토큰 주도록 변경이 필요 + if (castImages == null) { + castImages = List.of(); + } + if (staffImages == null) { + staffImages = List.of(); + } + if (performanceImages == null) { + performanceImages = List.of(); + } + + PerformanceMakerPresignedUrlFindAllResponse response = fileUseCase.issueAllPresignedUrlsForPerformanceMaker(posterImage, castImages, staffImages, performanceImages); + return ResponseEntity.ok(SuccessResponse.of(FileSuccessCode.PERFORMANCE_MAKER_PRESIGNED_URL_ISSUED, response)); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/application/FileService.java b/src/main/java/com/beat/global/external/s3/application/FileService.java new file mode 100644 index 00000000..0b3ffb75 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/application/FileService.java @@ -0,0 +1,115 @@ +package com.beat.global.external.s3.application; + +import com.amazonaws.HttpMethod; +import com.amazonaws.services.s3.AmazonS3; +import com.amazonaws.services.s3.model.GeneratePresignedUrlRequest; +import com.beat.global.external.s3.application.dto.PerformanceMakerPresignedUrlFindAllResponse; +import com.beat.global.external.s3.port.in.FileUseCase; +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.HashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Service +@RequiredArgsConstructor +public class FileService implements FileUseCase { + + @Value("${cloud.s3.bucket}") + private String bucket; + + private final AmazonS3 amazonS3; + + @Override + public PerformanceMakerPresignedUrlFindAllResponse issueAllPresignedUrlsForPerformanceMaker(String posterImage, List castImages, List staffImages, List performanceImages) { + Map> performanceMakerPresignedUrls = new HashMap<>(); + + // Poster Image URL + Map posterUrl = new HashMap<>(); + String posterFilePath = generatePath("poster", posterImage); + URL posterPresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, posterFilePath)); + posterUrl.put(posterImage, posterPresignedUrl.toString()); + performanceMakerPresignedUrls.put("poster", posterUrl); + + // Cast Images URLs + Map castUrls = new HashMap<>(); + for (String castImage : castImages) { + String castFilePath = generatePath("cast", castImage); + URL castPresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, castFilePath)); + castUrls.put(castImage, castPresignedUrl.toString()); + } + performanceMakerPresignedUrls.put("cast", castUrls); + + // Staff Images URLs + Map staffUrls = new HashMap<>(); + for (String staffImage : staffImages) { + String staffFilePath = generatePath("staff", staffImage); + URL staffPresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, staffFilePath)); + staffUrls.put(staffImage, staffPresignedUrl.toString()); + } + performanceMakerPresignedUrls.put("staff", staffUrls); + + // Performance Images URLs + Map performanceImageUrls = new HashMap<>(); + for (String performanceImage : performanceImages) { + String performanceImageFilePath = generatePath("performance", performanceImage); + URL performanceImagePresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, performanceImageFilePath)); + performanceImageUrls.put(performanceImage, performanceImagePresignedUrl.toString()); + } + performanceMakerPresignedUrls.put("performance", performanceImageUrls); + + return PerformanceMakerPresignedUrlFindAllResponse.from(performanceMakerPresignedUrls); + } + + // Carousel Images URLs + @Override + public Map issueAllPresignedUrlsForCarousel(List carouselImages) { + Map carouselPresignedUrls = new HashMap<>(); + + for (String carouselImage : carouselImages) { + String carouselFilePath = generatePath("carousel", carouselImage); + URL carouselPresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, carouselFilePath)); + carouselPresignedUrls.put(carouselImage, carouselPresignedUrl.toString()); + } + + return carouselPresignedUrls; + } + + // Banner Image URL + @Override + public String issuePresignedUrlForBanner(String bannerImage) { + String bannerFilePath = generatePath("banner", bannerImage); + URL bannerPresignedUrl = amazonS3.generatePresignedUrl(buildPresignedUrlRequest(bucket, bannerFilePath)); + + return bannerPresignedUrl.toString(); + } + + private GeneratePresignedUrlRequest buildPresignedUrlRequest(String bucket, String fileName) { + return new GeneratePresignedUrlRequest(bucket, fileName) + .withMethod(HttpMethod.PUT) + .withExpiration(generatePresignedUrlExpiration()); + } + + private Date generatePresignedUrlExpiration() { + Date expiration = new Date(); + long expTimeMillis = expiration.getTime(); + expTimeMillis += 1000 * 60 * 60 * 2; + expiration.setTime(expTimeMillis); + + return expiration; + } + + private String generateFileId() { + return UUID.randomUUID().toString(); + } + + private String generatePath(String prefix, String fileName) { + String fileId = generateFileId(); + return String.format("%s/%s", prefix, fileId + "-" + fileName); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/application/dto/BannerPresignedUrlFindResponse.java b/src/main/java/com/beat/global/external/s3/application/dto/BannerPresignedUrlFindResponse.java new file mode 100644 index 00000000..db79b0b9 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/application/dto/BannerPresignedUrlFindResponse.java @@ -0,0 +1,9 @@ +package com.beat.global.external.s3.application.dto; + +public record BannerPresignedUrlFindResponse( + String bannerPresignedUrl +) { + public static BannerPresignedUrlFindResponse from(String bannerPresignedUrl) { + return new BannerPresignedUrlFindResponse(bannerPresignedUrl); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/application/dto/CarouselPresignedUrlFindAllResponse.java b/src/main/java/com/beat/global/external/s3/application/dto/CarouselPresignedUrlFindAllResponse.java new file mode 100644 index 00000000..133be7f2 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/application/dto/CarouselPresignedUrlFindAllResponse.java @@ -0,0 +1,11 @@ +package com.beat.global.external.s3.application.dto; + +import java.util.Map; + +public record CarouselPresignedUrlFindAllResponse( + Map carouselPresignedUrls +) { + public static CarouselPresignedUrlFindAllResponse from(Map carouselPresignedUrls) { + return new CarouselPresignedUrlFindAllResponse(carouselPresignedUrls); + } +} diff --git a/src/main/java/com/beat/global/external/s3/application/dto/PerformanceMakerPresignedUrlFindAllResponse.java b/src/main/java/com/beat/global/external/s3/application/dto/PerformanceMakerPresignedUrlFindAllResponse.java new file mode 100644 index 00000000..6fca8735 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/application/dto/PerformanceMakerPresignedUrlFindAllResponse.java @@ -0,0 +1,11 @@ +package com.beat.global.external.s3.application.dto; + +import java.util.Map; + +public record PerformanceMakerPresignedUrlFindAllResponse( + Map> performanceMakerPresignedUrls +) { + public static PerformanceMakerPresignedUrlFindAllResponse from(Map> performanceMakerPresignedUrls) { + return new PerformanceMakerPresignedUrlFindAllResponse(performanceMakerPresignedUrls); + } +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/S3Config.java b/src/main/java/com/beat/global/external/s3/config/S3Config.java similarity index 96% rename from src/main/java/com/beat/global/external/s3/S3Config.java rename to src/main/java/com/beat/global/external/s3/config/S3Config.java index 73eca54e..ce079891 100644 --- a/src/main/java/com/beat/global/external/s3/S3Config.java +++ b/src/main/java/com/beat/global/external/s3/config/S3Config.java @@ -1,4 +1,4 @@ -package com.beat.global.external.s3; +package com.beat.global.external.s3.config; import com.amazonaws.auth.AWSStaticCredentialsProvider; import com.amazonaws.auth.BasicAWSCredentials; diff --git a/src/main/java/com/beat/global/external/s3/controller/FileController.java b/src/main/java/com/beat/global/external/s3/controller/FileController.java index eec8614e..e69de29b 100644 --- a/src/main/java/com/beat/global/external/s3/controller/FileController.java +++ b/src/main/java/com/beat/global/external/s3/controller/FileController.java @@ -1,37 +0,0 @@ -package com.beat.global.external.s3.controller; - -import com.beat.global.external.s3.service.FileService; -import io.swagger.v3.oas.annotations.Operation; -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; - -import java.util.List; -import java.util.Map; - -@RestController -@RequestMapping("/api/files") -@RequiredArgsConstructor -public class FileController { - - private final FileService fileService; - - @Operation(summary = "presigned-url API", description = "S3에 업로드 할 수 있는 유효한 url을 주는 GET API입니다.") - @GetMapping("/presigned-url") - public ResponseEntity>> getPresignedUrls( - @RequestParam String posterImage, - @RequestParam(required = false) List castImages, - @RequestParam(required = false) List staffImages) { - if (castImages == null) { - castImages = List.of(); - } - if (staffImages == null) { - staffImages = List.of(); - } - Map> response = fileService.getPresignedUrls(posterImage, castImages, staffImages); - return ResponseEntity.ok(response); - } -} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/exception/FileSuccessCode.java b/src/main/java/com/beat/global/external/s3/exception/FileSuccessCode.java new file mode 100644 index 00000000..fef7e359 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/exception/FileSuccessCode.java @@ -0,0 +1,13 @@ +package com.beat.global.external.s3.exception; + +import com.beat.global.common.exception.base.BaseSuccessCode; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FileSuccessCode implements BaseSuccessCode { + PERFORMANCE_MAKER_PRESIGNED_URL_ISSUED(200, "공연 메이커를 위한 Presigned URL 발급 성공"); + private final int status; + private final String message; +} \ No newline at end of file diff --git a/src/main/java/com/beat/global/external/s3/port/in/FileUseCase.java b/src/main/java/com/beat/global/external/s3/port/in/FileUseCase.java new file mode 100644 index 00000000..28adfec4 --- /dev/null +++ b/src/main/java/com/beat/global/external/s3/port/in/FileUseCase.java @@ -0,0 +1,17 @@ +package com.beat.global.external.s3.port.in; + +import com.beat.global.external.s3.application.dto.BannerPresignedUrlFindResponse; +import com.beat.global.external.s3.application.dto.CarouselPresignedUrlFindAllResponse; +import com.beat.global.external.s3.application.dto.PerformanceMakerPresignedUrlFindAllResponse; + +import java.util.List; +import java.util.Map; + +public interface FileUseCase { + + PerformanceMakerPresignedUrlFindAllResponse issueAllPresignedUrlsForPerformanceMaker(String posterImage, List castImages, List staffImages, List performanceImages); + + Map issueAllPresignedUrlsForCarousel(List carouselImages); + + String issuePresignedUrlForBanner(String bannerImage); +} \ 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 deleted file mode 100644 index fd544080..00000000 --- a/src/main/java/com/beat/global/external/s3/service/FileService.java +++ /dev/null @@ -1,80 +0,0 @@ -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 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.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; - -@Service -@RequiredArgsConstructor -public class FileService { - - @Value("${cloud.s3.bucket}") - private String bucket; - - private final AmazonS3 amazonS3; - - public Map> getPresignedUrls(String posterImage, List castImages, List staffImages) { - Map> presignedUrls = new HashMap<>(); - - // Poster Image URL - Map posterUrl = new HashMap<>(); - String posterFilePath = createPath("poster", posterImage); - URL posterPresignedUrl = amazonS3.generatePresignedUrl(getGeneratePresignedUrlRequest(bucket, posterFilePath)); - posterUrl.put(posterImage, posterPresignedUrl.toString()); - presignedUrls.put("poster", posterUrl); - - // Cast Images URLs - Map castUrls = new HashMap<>(); - for (String castImage : castImages) { - String castFilePath = createPath("cast", castImage); - URL castPresignedUrl = amazonS3.generatePresignedUrl(getGeneratePresignedUrlRequest(bucket, castFilePath)); - castUrls.put(castImage, castPresignedUrl.toString()); - } - presignedUrls.put("cast", castUrls); - - // Staff Images URLs - Map staffUrls = new HashMap<>(); - for (String staffImage : staffImages) { - String staffFilePath = createPath("staff", staffImage); - URL staffPresignedUrl = amazonS3.generatePresignedUrl(getGeneratePresignedUrlRequest(bucket, staffFilePath)); - staffUrls.put(staffImage, staffPresignedUrl.toString()); - } - presignedUrls.put("staff", staffUrls); - - return presignedUrls; - } - - 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 * 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); - } -} \ 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 index fa4e1bb6..f3ffc34b 100644 --- a/src/main/java/com/beat/global/swagger/SwaggerConfig.java +++ b/src/main/java/com/beat/global/swagger/SwaggerConfig.java @@ -6,11 +6,17 @@ import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; + +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SwaggerConfig { + + @Value("${app.server.url}") + private String serverUrl; + @Bean public OpenAPI openAPI() { String jwt = "JWT"; @@ -21,7 +27,7 @@ public OpenAPI openAPI() { .scheme("bearer") .bearerFormat("JWT") ); - return new OpenAPI().addServersItem(new Server().url("/")) + return new OpenAPI().addServersItem(new Server().url(serverUrl)) .components(new Components()) .info(apiInfo()) .addSecurityItem(securityRequirement) @@ -31,6 +37,6 @@ private Info apiInfo() { return new Info() .title("BEAT Project API") .description("간편하게 소규모 공연을 등록하고 관리할 수 있는 티켓 예매 플랫폼") - .version("1.0.0"); + .version("1.1.0"); } } \ No newline at end of file diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index c2309f23..e9d0c438 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -57,8 +57,8 @@ spring: jwt: secret: ${DEV_JWT_SECRET} - access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 - refresh-token-expire-time: 1209600000 # 14일 밀리초 + access-token-expire-time: ${DEV_ACCESS_TOKEN_EXPIRE_TIME} + refresh-token-expire-time: ${DEV_REFRESH_TOKEN_EXPIRE_TIME} cloud: aws: @@ -71,3 +71,13 @@ cloud: stack: auto: false +logging: + level: + root: info # 추후 debug로 변경 + +cors: + allowed-origins: ${DEV_ALLOWED_ORIGINS} + +app: + server: + url: ${DEV_SERVER_URL} \ No newline at end of file diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml deleted file mode 100644 index c923e123..00000000 --- a/src/main/resources/application-local.yml +++ /dev/null @@ -1,21 +0,0 @@ -server: - port: 8080 - -spring: - config: - activate: - on-profile: local - - datasource: - driver-class-name: com.mysql.cj.jdbc.Driver - url: jdbc:mysql://localhost:3306/beat - username: root - password: - jpa: - hibernate: - 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/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e21062b0..c9689bd2 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -57,8 +57,8 @@ spring: jwt: secret: ${PROD_JWT_SECRET} - access-token-expire-time: 31536000000 # 365일 (1년) 밀리초 - refresh-token-expire-time: 1209600000 # 14일 밀리초 + access-token-expire-time: ${PROD_ACCESS_TOKEN_EXPIRE_TIME} + refresh-token-expire-time: ${PROD_REFRESH_TOKEN_EXPIRE_TIME} cloud: aws: @@ -70,3 +70,14 @@ cloud: bucket: beat-prod-bucket stack: auto: false + +logging: + level: + root: info + +cors: + allowed-origins: ${PROD_ALLOWED_ORIGINS} + +app: + server: + url: ${PROD_SERVER_URL} \ 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 032ed530..ca1a66c5 100644 --- a/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java +++ b/src/test/java/com/beat/domain/booking/GuestBookingServiceConcurrencyTest.java @@ -3,6 +3,7 @@ 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.booking.domain.BookingStatus; import com.beat.domain.performance.dao.PerformanceRepository; import com.beat.domain.performance.domain.BankName; import com.beat.domain.performance.domain.Genre; @@ -129,7 +130,7 @@ public void testConcurrentGuestBooking() { "1990-01-01", generateRandomPassword(), 35000, - false + BookingStatus.CHECKING_PAYMENT ); GuestBookingResponse response = guestBookingService.createGuestBooking(request); assertNotNull(response); @@ -152,7 +153,7 @@ public void testConcurrentGuestBooking() { "1990-01-01", generateRandomPassword(), 35000, - false + BookingStatus.CHECKING_PAYMENT ); GuestBookingResponse response = guestBookingService.createGuestBooking(request); assertNotNull(response);