diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml new file mode 100644 index 00000000..c4dddfa0 --- /dev/null +++ b/.github/workflows/gradle.yml @@ -0,0 +1,188 @@ +name: CI/CD Github Action + +on: + push: + branches: [ "Master", "Weekly/*" ] + pull_request_target: + branches: [ "Master", "Weekly/*" ] + +permissions: + contents: read + +env: + GOOGLE_CLIENT_ID : ${{ secrets.ENV_GOOGLE_CLIENT_ID }} + GOOGLE_CLIENT_SECRET: ${{ secrets.ENV_GOOGLE_CLIENT_SECRET }} + GOOGLE_REDIRECT_URI : ${{ secrets.ENV_GOOGLE_REDIRECT_URI }} + JWT_SECRET : ${{ secrets.ENV_JWT_SECRET }} + KAKAO_CLIENT_ID : ${{ secrets.ENV_KAKAO_CLIENT_ID }} + KAKAO_CLIENT_SECRET : ${{ secrets.ENV_KAKAO_CLIENT_SECRET }} + KAKAO_REDIRECT_URI : ${{ secrets.ENV_KAKAO_REDIRECT_URI }} + KAKAOPAY_SECRET_KEY : ${{ secrets.ENV_KAKAOPAY_SECRET_KEY }} + +jobs: + ## 1단계: 프로젝트 빌드 + Build: + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'corretto' + + - name: Gradle Caching (for faster build) + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Build Application + run: | + chmod +x gradlew + ./gradlew clean build -x test + + - name: Store build failure reports (execute when build fail) + if: failure() + uses: actions/upload-artifact@v3 + with: + name: build-failure-reports + path: | + **/build/reports/ + + - name: Store build artifacts + uses: actions/upload-artifact@v3 + with: + name: build-artifacts + path: build/libs/*.jar + + ## 2단계: 테스트 실행 + Test: + runs-on: ubuntu-22.04 + needs: Build # Build 단계가 완료되어야 실행됨 + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'corretto' + + - name: Gradle Caching (for faster build) + uses: actions/cache@v3 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Test Application + run: | + chmod +x gradlew + ./gradlew test + + - name: Store test failure reports (execute when test fail) + if: failure() + uses: actions/upload-artifact@v3 + with: + name: test-failure-reports + path: | + **/build/test-results/ + + ## 3단계: Docker 빌드 및 푸시 + Docker-Build: + runs-on: ubuntu-22.04 + needs: Test # Test 단계가 성공적으로 완료되어야 실행됨 + if: github.event_name == 'push' + steps: + - uses: actions/checkout@v4 + + - name: Download Build Artifacts + uses: actions/download-artifact@v3 + with: + name: build-artifacts + path: build/libs + + - name: Docker Hub Login + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Docker Build + run: docker build -f Dockerfile --build-arg DEPENDENCY=build/dependency -t ${{ secrets.DOCKER_REPO_FULLNAME }} . + + - name: Docker Push + run: docker push ${{ secrets.DOCKER_REPO_FULLNAME }} + + ## 4단계: 서버에 배포 + Deploy: + runs-on: ubuntu-22.04 + needs: Docker-Build + if: github.event_name == 'push' + steps: + - name: Update .env + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SSH_PORT }} + script: | + echo "GOOGLE_CLIENT_ID=${{ secrets.ENV_GOOGLE_CLIENT_ID }}" > ~/.env + echo "GOOGLE_CLIENT_SECRET=${{ secrets.ENV_GOOGLE_CLIENT_SECRET }}" >> ~/.env + echo "GOOGLE_REDIRECT_URI=${{ secrets.ENV_GOOGLE_REDIRECT_URI }}" >> ~/.env + echo "JWT_SECRET=${{ secrets.ENV_JWT_SECRET }}" >> ~/.env + echo "KAKAO_CLIENT_ID=${{ secrets.ENV_KAKAO_CLIENT_ID }}" >> ~/.env + echo "KAKAO_CLIENT_SECRET=${{ secrets.ENV_KAKAO_CLIENT_SECRET }}" >> ~/.env + echo "KAKAO_REDIRECT_URI=${{ secrets.ENV_KAKAO_REDIRECT_URI }}" >> ~/.env + echo "KAKAOPAY_SECRET_KEY=${{ secrets.ENV_KAKAOPAY_SECRET_KEY }}" >> ~/.env + echo "Environment setup has been completed." + + - name: Pull New Docker Image + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SSH_PORT }} + envs: GITHUB_SHA + script: sudo docker pull ${{ secrets.DOCKER_REPO_FULLNAME }} + + - name: Stop Old Docker Image + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SSH_PORT }} + envs: GITHUB_SHA + script: sudo docker stop would-you-in + + - name: Run New Docker Image + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SSH_PORT }} + envs: GITHUB_SHA + script: sudo docker run --rm -d -p 80:8080 --env-file ~/.env --name would-you-in ${{ secrets.DOCKER_REPO_FULLNAME }} + + - name: Clean-Up Docker Image + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + port: ${{ secrets.SSH_PORT }} + envs: GITHUB_SHA + script: sudo docker image prune -f \ No newline at end of file diff --git a/.gitignore b/.gitignore index 133bbac4..f4f1f4ac 100644 --- a/.gitignore +++ b/.gitignore @@ -38,3 +38,6 @@ out/ ### static images /src/main/resources/staticimages/ + +### env file +.env \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..09ba7c74 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +# open jdk 21 버전의 환경을 구성한다. +FROM openjdk:21-jdk-slim + +# build가 될 때 JAR_FILE이라는 변수 명에 build/libs/*.jar 선언 +# build/libs - gradle로 빌드했을 때 jar 파일이 생성되는 경로임 +ARG JAR_FILE=build/libs/*.jar + +# JAR_FILE을 agaproject.jar로 복사 (이 부분(.jar)은 개발환경에 따라 다름) +COPY ${JAR_FILE} wouldyouin.jar + +# 운영 및 개발에서 사용되는 환경 설정을 분리한다. +ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=deploy", "/wouldyouin.jar"] \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/ImageController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/ImageController.java index c0dfca91..cc4fa869 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/ImageController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/ImageController.java @@ -7,9 +7,13 @@ import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.api.dto.ImageResponse; import org.ktc2.cokaen.wouldyouin.Image.application.ImageServiceFactory; +import org.ktc2.cokaen.wouldyouin.Image.application.ImageStorage; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; import org.ktc2.cokaen.wouldyouin._common.exception.FailToReadImageException; +import org.ktc2.cokaen.wouldyouin.auth.Authorize; +import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; @@ -29,8 +33,10 @@ public class ImageController { private final ImageServiceFactory imageServiceFactory; + // Todo: authorize @PostMapping("/images") - public ResponseEntity<ApiResponseBody<List<ImageResponse>>> uploadImages(@RequestParam List<MultipartFile> images, + public ResponseEntity<ApiResponseBody<List<ImageResponse>>> uploadImages( + @RequestParam List<MultipartFile> images, @RequestParam(value = "type") ImageDomain imageDomain) { return ApiResponse.ok(imageServiceFactory.getImageServiceByImageType(imageDomain).saveAndCreateImages(images)); } @@ -44,8 +50,11 @@ public ResponseEntity<byte[]> getImage(@PathVariable String path) { } } + // Todo: authorize @DeleteMapping("/images/{id}") - public ResponseEntity<ApiResponseBody<Void>> deleteImage(@PathVariable Long id, @RequestParam ImageDomain imageDomain) { + public ResponseEntity<ApiResponseBody<Void>> deleteImage( + @PathVariable Long id, + @RequestParam(value = "type") ImageDomain imageDomain) { imageServiceFactory.getImageServiceByImageType(imageDomain).deleteAndDelete(id); return ApiResponse.noContent(); } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/dto/ImageResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/dto/ImageResponse.java index 9a0c2df6..5c219943 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/dto/ImageResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/api/dto/ImageResponse.java @@ -14,10 +14,11 @@ public class ImageResponse { private LocalDateTime createdDate; private Long size; - public static ImageResponse from(Image image) { + public static ImageResponse from(Image image, String domainName) { + System.out.println("도메인" + domainName); return ImageResponse.builder() .id(image.getId()) - .url(image.getUrl()) + .url(domainName + image.getUrl()) .size(image.getSize()) .createdDate(image.getCreatedDate()) .build(); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/AdvertisementImageService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/AdvertisementImageService.java index c8319b26..967b22a1 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/AdvertisementImageService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/AdvertisementImageService.java @@ -18,10 +18,10 @@ @RequiredArgsConstructor public class AdvertisementImageService extends ImageService<AdvertisementImage> { - @Value("${image.upload.ad.sub-path}") - private String subPath; private final ImageStorage imageStorage; private final AdvertisementImageRepository adImageRepository; + @Value("${image.upload.ad.sub-path}") + private String subPath; @Override protected ImageRepository<AdvertisementImage> getImageRepository() { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/CurationImageService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/CurationImageService.java index a1d77bb6..518c2a58 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/CurationImageService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/CurationImageService.java @@ -40,7 +40,7 @@ protected String getSubPath() { @Override protected CurationImage toEntity(ImageRequest imageRequest) { return CurationImage.builder() - .name(imageRequest.getUrl()) + .url(imageRequest.getUrl()) .size(imageRequest.getSize()) .build(); } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/EventImageService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/EventImageService.java index 13367b66..ecfc2a77 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/EventImageService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/EventImageService.java @@ -1,18 +1,13 @@ package org.ktc2.cokaen.wouldyouin.Image.application; -import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.api.ImageDomain; import org.ktc2.cokaen.wouldyouin.Image.api.dto.ImageRequest; -import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; import org.ktc2.cokaen.wouldyouin.Image.persist.EventImage; import org.ktc2.cokaen.wouldyouin.Image.persist.EventImageRepository; import org.ktc2.cokaen.wouldyouin.Image.persist.ImageRepository; -import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; import org.ktc2.cokaen.wouldyouin._common.exception.EntityParamIsNullException; -import org.ktc2.cokaen.wouldyouin.advertisement.persist.Advertisement; -import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; @@ -44,7 +39,7 @@ protected String getSubPath() { @Override protected EventImage toEntity(ImageRequest imageRequest) { - return EventImage.builder().name(imageRequest.getUrl()).size(imageRequest.getSize()).build(); + return EventImage.builder().url(imageRequest.getUrl()).size(imageRequest.getSize()).build(); } @Transactional diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageService.java index 8a4e495c..aaac70b4 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageService.java @@ -9,6 +9,7 @@ import org.ktc2.cokaen.wouldyouin.Image.persist.ImageRepository; import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -20,6 +21,9 @@ public abstract class ImageService<T extends Image> { @Autowired protected ImageStorage imageStorage; + @Value("${spring.wouldyouin-domain-name}") + private String domainName; + protected abstract ImageRepository<T> getImageRepository(); protected abstract ImageDomain getImageDomain(); @@ -28,13 +32,13 @@ public abstract class ImageService<T extends Image> { protected abstract T toEntity(ImageRequest imageRequest); - public T getByIdOrThrow(Long id) { + public T getById(Long id) { return getImageRepository().findById(id) .orElseThrow(() -> new EntityNotFoundException(getImageDomain().name() + " Image")); } protected ImageResponse create(ImageRequest imageRequest) { - return ImageResponse.from(getImageRepository().save(toEntity(imageRequest))); + return ImageResponse.from(getImageRepository().save(toEntity(imageRequest)), domainName); } protected void delete(Long id) { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageStorage.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageStorage.java index 9a11128c..046aea29 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageStorage.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageStorage.java @@ -4,11 +4,16 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; +import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.UUID; -import org.ktc2.cokaen.wouldyouin.Image.api.ImageDomain; +import org.ktc2.cokaen.wouldyouin._common.exception.FailedToDeleteImageException; +import org.ktc2.cokaen.wouldyouin._common.exception.FailedToUploadImageException; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; import org.springframework.web.multipart.MultipartFile; @Component @@ -18,13 +23,38 @@ public class ImageStorage { private String commonPath; public String save(MultipartFile image, String subPath) { - String fileName = generateUuidName() + "." + getExtension(image); - Path path = Paths.get(commonPath, subPath, fileName); + String fileName = ""; try { + fileName = generateUuidName() + "." + getExtension(image); + Path path = Paths.get(commonPath, subPath, fileName); Files.createDirectories(path.getParent()); Files.write(path, image.getBytes()); } catch (IOException ex) { - throw new RuntimeException("failed to upload image."); + throw new FailedToUploadImageException(); + } + return subPath + "/" + fileName; + } + + // Todo: payment랑 이부분 restclient 유틸로 빼기 + public String save(String imageUrl, String subPath) { + RestClient client = RestClient.builder().build(); + String fileName = ""; + try { + ResponseEntity<byte[]> response = client.get() + .uri(imageUrl) + .retrieve() + .toEntity(byte[].class); + + if (Optional.ofNullable(response).isPresent() && response.getStatusCode().is2xxSuccessful()) { + byte[] imageBytes = response.getBody(); + + fileName = generateUuidName() + "." + getExtension(imageUrl); + Path path = Paths.get(commonPath, subPath, fileName); + Files.createDirectories(path.getParent()); + Files.write(path, response.getBody()); + } + } catch (IOException ex) { + throw new FailedToUploadImageException(); } return subPath + "/" + fileName; } @@ -33,7 +63,7 @@ public void delete(String imagePath) { try { Files.deleteIfExists(Paths.get(imagePath)); } catch (IOException ex) { - throw new RuntimeException("failed to delete image."); + throw new FailedToDeleteImageException(); } } @@ -41,6 +71,11 @@ protected static String getExtension(MultipartFile image) { return Objects.requireNonNull(image.getContentType()).split("/")[1]; } + protected static String getExtension(String imageUrl) { + String[] splitted = imageUrl.split("\\."); + return splitted[splitted.length - 1]; + } + private static String generateUuidName() { return UUID.randomUUID().toString().replace("-", ""); } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageUrlToMemberImageListConverter.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageUrlToMemberImageListConverter.java deleted file mode 100644 index 71e39398..00000000 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/ImageUrlToMemberImageListConverter.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.ktc2.cokaen.wouldyouin.Image.application; - - -import java.util.List; -import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; - -public interface ImageUrlToMemberImageListConverter { - MemberImage convert(String imageUrl); -} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/MemberImageService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/MemberImageService.java index 72377834..621679b3 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/MemberImageService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/application/MemberImageService.java @@ -1,25 +1,28 @@ package org.ktc2.cokaen.wouldyouin.Image.application; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.api.ImageDomain; import org.ktc2.cokaen.wouldyouin.Image.api.dto.ImageRequest; -import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; import org.ktc2.cokaen.wouldyouin.Image.persist.ImageRepository; import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImageRepository; -import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; import org.ktc2.cokaen.wouldyouin._common.exception.EntityParamIsNullException; -import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; import org.ktc2.cokaen.wouldyouin.member.persist.BaseMember; import org.ktc2.cokaen.wouldyouin.member.persist.BaseMemberRepository; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.web.client.RestClient; @Service @RequiredArgsConstructor -public class MemberImageService extends ImageService<MemberImage> { +public class MemberImageService extends ImageService<MemberImage> { private final MemberImageRepository memberImageRepository; private final BaseMemberRepository baseMemberRepository; @@ -45,7 +48,7 @@ protected String getSubPath() { @Override protected MemberImage toEntity(ImageRequest imageRequest) { return MemberImage.builder() - .name(imageRequest.getUrl()) + .url(imageRequest.getUrl()) .size(imageRequest.getSize()) .build(); } @@ -58,11 +61,9 @@ public void setBaseMember(MemberImage image, BaseMember member) { } // TODO: imageUrl을 MemberImage로 변환하는 로직 추가 필요 + // Todo: extension과 size 불러오기 public MemberImage convert(String imageUrl) { - return MemberImage.builder() - .name("http://example.com/images/MockMemberImageUrl") - .size(10L) - .extension(".jpeg") - .build(); + var request = ImageRequest.of(imageStorage.save(imageUrl, subPath), 123123L, "jpg"); + return memberImageRepository.save(toEntity(request)); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/AdvertisementImage.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/AdvertisementImage.java index 6d6d58c6..2fd5c836 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/AdvertisementImage.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/AdvertisementImage.java @@ -1,6 +1,7 @@ package org.ktc2.cokaen.wouldyouin.Image.persist; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import lombok.Builder; @@ -13,7 +14,7 @@ @NoArgsConstructor public class AdvertisementImage extends Image { - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "advertisement_id") private Advertisement advertisement; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/CurationImage.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/CurationImage.java index 1ebe0cd2..aee7d07d 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/CurationImage.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/CurationImage.java @@ -1,13 +1,12 @@ package org.ktc2.cokaen.wouldyouin.Image.persist; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import lombok.Builder; import lombok.NoArgsConstructor; import lombok.Setter; -import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; @Entity @@ -15,12 +14,12 @@ @NoArgsConstructor public class CurationImage extends Image { - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "curation_card_id") private CurationCard curationCard; @Builder - public CurationImage(String name, Long size, String extension) { - super(name, size, extension); + public CurationImage(String url, Long size, String extension) { + super(url, size, extension); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/EventImage.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/EventImage.java index 4c079e7f..ce8d1265 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/EventImage.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/EventImage.java @@ -1,6 +1,7 @@ package org.ktc2.cokaen.wouldyouin.Image.persist; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import lombok.Builder; @@ -13,12 +14,12 @@ @NoArgsConstructor public class EventImage extends Image { - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "event_id") private Event event; @Builder - public EventImage(String name, Long size, String extension) { - super(name, size, extension); + public EventImage(String url, Long size, String extension) { + super(url, size, extension); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/MemberImage.java b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/MemberImage.java index d36714ba..30158439 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/MemberImage.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/Image/persist/MemberImage.java @@ -1,6 +1,7 @@ package org.ktc2.cokaen.wouldyouin.Image.persist; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.JoinColumn; import jakarta.persistence.OneToOne; import lombok.Builder; @@ -13,13 +14,12 @@ @NoArgsConstructor public class MemberImage extends Image { - @OneToOne + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "base_member_id") - @Setter private BaseMember baseMember; @Builder - public MemberImage(String name, Long size, String extension) { - super(name, size, extension); + public MemberImage(String url, Long size, String extension) { + super(url, size, extension); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/api/SliceInfo.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/api/SliceInfo.java new file mode 100644 index 00000000..303194f8 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/api/SliceInfo.java @@ -0,0 +1,14 @@ +package org.ktc2.cokaen.wouldyouin._common.api; + +import lombok.Builder; +import lombok.Getter; +import lombok.Setter; + +@Setter +@Getter +@Builder +public class SliceInfo { + + private final Integer sliceSize; + private final Long lastId; +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/config/ParamDefaults.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/config/ParamDefaults.java new file mode 100644 index 00000000..5e4bfdd3 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/config/ParamDefaults.java @@ -0,0 +1,11 @@ +package org.ktc2.cokaen.wouldyouin._common.config; + +public class ParamDefaults { + + public static final String PAGE = "0"; + public static final String PAGE_SIZE = "10"; + public static final String LAST_ID = Long.MAX_VALUE + ""; + public static final String TITLE = ""; + public static final String AREA = "전체"; + public static final String CATEGORY = "전체"; +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/converter/HashtagConverter.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/converter/HashtagConverter.java index 69dc4e76..b3c4c268 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/converter/HashtagConverter.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/converter/HashtagConverter.java @@ -27,4 +27,4 @@ public List<String> convertToEntityAttribute(String dbData) { } return Arrays.stream(dbData.split(DELIMITER)).collect(Collectors.toList()); } -} +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/ErrorCode.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/ErrorCode.java index d155e76b..1e52d1d9 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/ErrorCode.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/ErrorCode.java @@ -16,7 +16,17 @@ public enum ErrorCode { FAIL_TO_READ_IMAGE(HttpStatus.CONFLICT.value(), "-20400", "Fail to read image"), - ENTITY_PARAM_IS_NULL(HttpStatus.BAD_REQUEST.value(), "-20400", "%s is null"); + FAIL_TO_PAY(HttpStatus.CONFLICT.value(), "-20400", "Fail to %s"), + + UNAUTHORIZED(HttpStatus.UNAUTHORIZED.value(), "-20400", "Unauthorized, %s."), + + ENTITY_PARAM_IS_NULL(HttpStatus.BAD_REQUEST.value(), "-20400", "%s is null"), + + FAIL_TO_UPLOAD_IMAGE(HttpStatus.CONFLICT.value(), "-20400", "Fail to upload image"), + + FAIL_TO_DELETE_IMAGE(HttpStatus.CONFLICT.value(), "-20400", "Fail to delete image"), + + CURRENT_LOCATION_EMPTY(HttpStatus.BAD_REQUEST.value(), "-20400", "현재 위치 정보를 찾을 수 없습니다."); private final Integer status; private final String code; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/GlobalExceptionHandler.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/GlobalExceptionHandler.java index 0dada627..14949f8e 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/GlobalExceptionHandler.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/error/GlobalExceptionHandler.java @@ -5,6 +5,7 @@ import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; import org.ktc2.cokaen.wouldyouin._common.exception.BusinessException; +import org.springframework.context.support.DefaultMessageSourceResolvable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.annotation.ControllerAdvice; @@ -21,7 +22,7 @@ public ResponseEntity<ApiResponseBody<Void>> handleBusinessException(BusinessExc @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ApiResponseBody<Void>> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { return ApiResponse.error(ErrorCode.INVALID_INPUT_VALUE, e.getBindingResult().getFieldErrors().stream() - .map(error -> String.format("%s %s", error.getField(), error.getDefaultMessage())) - .collect(Collectors.joining(","))); + .map(DefaultMessageSourceResolvable::getDefaultMessage) + .collect(Collectors.joining(", "))); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/CurrentLocationEmptyException.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/CurrentLocationEmptyException.java new file mode 100644 index 00000000..28e1effd --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/CurrentLocationEmptyException.java @@ -0,0 +1,10 @@ +package org.ktc2.cokaen.wouldyouin._common.exception; + +import org.ktc2.cokaen.wouldyouin._common.error.ErrorCode; + +public class CurrentLocationEmptyException extends BusinessException { + + public CurrentLocationEmptyException() { + super(ErrorCode.CURRENT_LOCATION_EMPTY, "현재 위치 정보를 찾을 수 없습니다."); + } +} diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToDeleteImageException.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToDeleteImageException.java new file mode 100644 index 00000000..e470f6d1 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToDeleteImageException.java @@ -0,0 +1,10 @@ +package org.ktc2.cokaen.wouldyouin._common.exception; + +import org.ktc2.cokaen.wouldyouin._common.error.ErrorCode; + +public class FailedToDeleteImageException extends BusinessException { + + public FailedToDeleteImageException() { + super(ErrorCode.FAIL_TO_DELETE_IMAGE, ErrorCode.FAIL_TO_DELETE_IMAGE.getMessage()); + } +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToPayException.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToPayException.java new file mode 100644 index 00000000..76aa3a3d --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToPayException.java @@ -0,0 +1,10 @@ +package org.ktc2.cokaen.wouldyouin._common.exception; + +import org.ktc2.cokaen.wouldyouin._common.error.ErrorCode; + +public class FailedToPayException extends BusinessException { + + public FailedToPayException(String message) { + super(ErrorCode.FAIL_TO_PAY, message); + } +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToUploadImageException.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToUploadImageException.java new file mode 100644 index 00000000..813b211a --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/FailedToUploadImageException.java @@ -0,0 +1,10 @@ +package org.ktc2.cokaen.wouldyouin._common.exception; + +import org.ktc2.cokaen.wouldyouin._common.error.ErrorCode; + +public class FailedToUploadImageException extends BusinessException { + + public FailedToUploadImageException() { + super(ErrorCode.FAIL_TO_UPLOAD_IMAGE, ErrorCode.FAIL_TO_UPLOAD_IMAGE.getMessage()); + } +} diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/UnauthorizedException.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/UnauthorizedException.java new file mode 100644 index 00000000..b7abfe7f --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/exception/UnauthorizedException.java @@ -0,0 +1,10 @@ +package org.ktc2.cokaen.wouldyouin._common.exception; + +import org.ktc2.cokaen.wouldyouin._common.error.ErrorCode; + +public class UnauthorizedException extends BusinessException { + + public UnauthorizedException(String message) { + super(message, ErrorCode.UNAUTHORIZED); + } +} diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/persist/Category.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/persist/Category.java index 17743177..5d30389b 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/persist/Category.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/persist/Category.java @@ -1,6 +1,7 @@ package org.ktc2.cokaen.wouldyouin._common.persist; public enum Category { + 전체, 밴드, 연극, 뮤지컬, diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Area.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Area.java new file mode 100644 index 00000000..412e18c8 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Area.java @@ -0,0 +1,21 @@ +package org.ktc2.cokaen.wouldyouin._common.vo; + +public enum Area { + 전체, + 서울, + 인천, + 경기도, + 강원도, + 충청남도, + 충청북도, + 경상북도, + 대전, + 대구, + 울산, + 전라북도, + 경상남도, + 부산, + 광주, + 전라남도, + 제주도 +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Category.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Category.java new file mode 100644 index 00000000..55593840 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Category.java @@ -0,0 +1,12 @@ +package org.ktc2.cokaen.wouldyouin._common.vo; + +public enum Category { + 전체, + 밴드, + 연극, + 뮤지컬, + 원데이클래스, + 전시회, + 공예, + 축제 +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Location.java b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Location.java new file mode 100644 index 00000000..240d31d5 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/_common/vo/Location.java @@ -0,0 +1,17 @@ +package org.ktc2.cokaen.wouldyouin._common.vo; + +import jakarta.persistence.Embeddable; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +@Embeddable +public class Location { + Double longitude; + Double latitude; +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/api/AdvertisementController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/api/AdvertisementController.java index 71866811..e8beda9e 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/api/AdvertisementController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/api/AdvertisementController.java @@ -8,6 +8,9 @@ import org.ktc2.cokaen.wouldyouin.advertisement.api.dto.AdvertisementRequest; import org.ktc2.cokaen.wouldyouin.advertisement.api.dto.AdvertisementResponse; import org.ktc2.cokaen.wouldyouin.advertisement.application.AdvertisementService; +import org.ktc2.cokaen.wouldyouin.auth.Authorize; +import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; @@ -28,7 +31,7 @@ public class AdvertisementController { private final AdvertisementService advertisementService; @GetMapping - public ResponseEntity<ApiResponseBody<List<AdvertisementResponse>>> getActiveAdvertisement() { + public ResponseEntity<ApiResponseBody<List<AdvertisementResponse>>> getActiveAdvertisements() { return ApiResponse.ok(advertisementService.getAllActiveAdvertisements()); } @@ -41,7 +44,8 @@ public ResponseEntity<ApiResponseBody<AdvertisementResponse>> getAdvertisementBy @PostMapping(consumes = {MediaType.APPLICATION_JSON_VALUE, MediaType.MULTIPART_FORM_DATA_VALUE}) public ResponseEntity<ApiResponseBody<AdvertisementResponse>> createAdvertisement( @Valid @RequestPart AdvertisementRequest advertisementRequest, - @RequestPart(required = false) MultipartFile image) { + @RequestPart(required = false) MultipartFile image, + @Authorize(MemberType.admin) MemberIdentifier memberIdentifier) { return ApiResponse.created(advertisementService.create(advertisementRequest, image)); } @@ -49,12 +53,15 @@ public ResponseEntity<ApiResponseBody<AdvertisementResponse>> createAdvertisemen public ResponseEntity<ApiResponseBody<AdvertisementResponse>> updateAdvertisement( @PathVariable Long adId, @Valid @RequestPart AdvertisementRequest advertisementRequest, - @RequestPart(required = false) MultipartFile image) { + @RequestPart(required = false) MultipartFile image, + @Authorize(MemberType.admin) MemberIdentifier memberIdentifier) { return ApiResponse.ok(advertisementService.update(adId, advertisementRequest, image)); } @DeleteMapping("/{adId}") - public ResponseEntity<ApiResponseBody<Void>> deleteAdvertisement(@PathVariable Long adId) { + public ResponseEntity<ApiResponseBody<Void>> deleteAdvertisement( + @PathVariable Long adId, + @Authorize(MemberType.admin) MemberIdentifier memberIdentifier) { advertisementService.delete(adId); return ApiResponse.noContent(); } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/application/AdvertisementService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/application/AdvertisementService.java index 7052b761..f0f63274 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/application/AdvertisementService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/application/AdvertisementService.java @@ -16,7 +16,7 @@ import org.springframework.web.multipart.MultipartFile; @Service -@RequiredArgsConstructor() +@RequiredArgsConstructor public class AdvertisementService { private final AdvertisementRepository adRepository; @@ -24,15 +24,10 @@ public class AdvertisementService { @Transactional(readOnly = true) public List<AdvertisementResponse> getAllActiveAdvertisements() { - return adRepository.findByCurrentTime(LocalDateTime.now()).stream() + return adRepository.findAllActiveAdvertisements(LocalDateTime.now()).stream() .map(AdvertisementResponse::from).toList(); } - @Transactional(readOnly = true) - public Advertisement getByIdOrThrow(Long id) throws RuntimeException { - return adRepository.findById(id).orElseThrow(RuntimeException::new); - } - @Transactional(readOnly = true) public AdvertisementResponse getAdvertisementByAdId(Long adId) { return AdvertisementResponse.from(adRepository.findById(adId).orElseThrow(() -> new EntityNotFoundException("Advertisement"))); @@ -51,25 +46,26 @@ public AdvertisementResponse create(AdvertisementRequest adRequest, MultipartFil // Todo: 롤백될 경우, 저장한 이미지 삭제 @Transactional public AdvertisementResponse update(Long adId, AdvertisementRequest adRequest, MultipartFile multipartFile) { - Advertisement target = adRepository.findById(adId).orElseThrow(() -> new EntityNotFoundException("Advertisement")); + Advertisement ad = adRepository.findById(adId).orElseThrow(() -> new EntityNotFoundException("Advertisement")); Optional.ofNullable(multipartFile).ifPresentOrElse( image -> { - adImageService.deleteAndDelete(target.getAdvertisementImage().getId()); + adImageService.deleteAndDelete(ad.getAdvertisementImage().getId()); AdvertisementImage adImage = adImageService.saveAndCreateImage(image); - target.updateFrom(adRequest, adImage); - adImage.setAdvertisement(target); + ad.updateFrom(adRequest, adImage); + adImage.setAdvertisement(ad); }, () -> { - AdvertisementImage adImage = target.getAdvertisementImage(); - target.updateFrom(adRequest, adImage); + AdvertisementImage adImage = ad.getAdvertisementImage(); + ad.updateFrom(adRequest, adImage); } ); - return AdvertisementResponse.from(target); + return AdvertisementResponse.from(ad); } @Transactional public void delete(Long adId) { - adRepository.findById(adId).orElseThrow(() -> new EntityNotFoundException("Advertisement")); + Advertisement ad = adRepository.findById(adId).orElseThrow(() -> new EntityNotFoundException("Advertisement")); + adImageService.deleteAndDelete(ad.getAdvertisementImage().getId()); adRepository.deleteById(adId); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/Advertisement.java b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/Advertisement.java index 17888c69..a22b1bd5 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/Advertisement.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/Advertisement.java @@ -1,5 +1,6 @@ package org.ktc2.cokaen.wouldyouin.advertisement.persist; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -13,6 +14,7 @@ import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; +import org.hibernate.annotations.Comment; import org.ktc2.cokaen.wouldyouin.Image.persist.AdvertisementImage; import org.ktc2.cokaen.wouldyouin.advertisement.api.dto.AdvertisementRequest; @@ -24,9 +26,11 @@ public class Advertisement { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "advertisement_id") private Long id; @NotNull + @Column(name = "title") private String title; @NotNull @@ -35,9 +39,11 @@ public class Advertisement { private AdvertisementImage advertisementImage; @NotNull + @Column(name = "start_time") private LocalDateTime startTime; @NotNull + @Column(name = "end_time") private LocalDateTime endTime; @Builder diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/AdvertisementRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/AdvertisementRepository.java index 01e7d5ca..c0a3c4cd 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/AdvertisementRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/advertisement/persist/AdvertisementRepository.java @@ -9,6 +9,6 @@ @Repository public interface AdvertisementRepository extends JpaRepository<Advertisement, Long> { - @Query("SELECT a FROM Advertisement a WHERE a.endTime > :currentTime") - List<Advertisement> findByCurrentTime(LocalDateTime currentTime); -} + @Query("SELECT a FROM Advertisement a JOIN FETCH a.advertisementImage WHERE a.endTime > :currentTime") + List<Advertisement> findAllActiveAdvertisements(LocalDateTime currentTime); +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/auth/AuthorizeArgumentResolver.java b/src/main/java/org/ktc2/cokaen/wouldyouin/auth/AuthorizeArgumentResolver.java index a20ade16..cef846b8 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/auth/AuthorizeArgumentResolver.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/auth/AuthorizeArgumentResolver.java @@ -3,6 +3,7 @@ import java.util.List; import java.util.Objects; import lombok.RequiredArgsConstructor; +import org.ktc2.cokaen.wouldyouin._common.exception.UnauthorizedException; import org.ktc2.cokaen.wouldyouin.auth.persist.CustomUserDetails; import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.springframework.core.MethodParameter; @@ -54,6 +55,6 @@ private void validateMemberType(List<MemberType> required, MemberType actual) { if (actual == MemberType.admin || required.contains(actual)) { return; } - throw new RuntimeException("요구된 멤버 형식과 실제 형식이 다릅니다."); + throw new UnauthorizedException("요구된 멤버 형식과 실제 형식이 다릅니다."); } } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/auth/application/AuthService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/auth/application/AuthService.java index a7114da0..127a50a1 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/auth/application/AuthService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/auth/application/AuthService.java @@ -2,9 +2,6 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; -import org.ktc2.cokaen.wouldyouin.Image.application.ImageUrlToMemberImageListConverter; -import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; -import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; import org.ktc2.cokaen.wouldyouin.auth.application.dto.LocalLoginRequest; import org.ktc2.cokaen.wouldyouin.auth.application.dto.LocalSignupRequest; @@ -32,7 +29,6 @@ public class AuthService { private final MemberService memberService; private final HostService hostService; private final OauthRequestServiceFactory oauthRequestServiceFactory; - private final MemberImageService memberImageService; @Transactional public TokenResponse localSignup(LocalSignupRequest request) { @@ -52,35 +48,37 @@ public SocialTokenResponse socialLogin(AccountType accountType, String code) { baseMemberService.checkUniqueEmailOrThrow(resources.getEmail()); - if (identifier.isEmpty()) { - // 소셜 계정의 회원가입 처리 - MemberResponse welcomeMemberResponse = memberService.createMember(MemberCreateRequest.builder() - .nickname(resources.getNickname()) - .email(resources.getEmail()) - .socialId(resources.getSocialId()) - .accountType(accountType) - .profileImage(memberImageService.convert(resources.getProfileImageUrl())) - .build()); - - return SocialTokenResponse.builder() - .isWelcomeMember(true) - .token(createToken(welcomeMemberResponse)) - .build(); + if (identifier.isPresent()) { + MemberIdentifier id = identifier.get(); + // 소셜 계정이고 추가 정보도 기입된 경우 + if (id.type() != MemberType.welcome) { + return SocialTokenResponse.builder() + .isWelcomeMember(false) + .token(createToken(id)) + .build(); + } + // 소셜 계정이지만 아직 추가 정보 기입이 되지 않은 경우 + else { + return SocialTokenResponse.builder() + .isWelcomeMember(true) + .token(createToken(id)) + .build(); + } + } - } else if (identifier.get().type() == MemberType.welcome) { - // 소셜 계정이지만 아직 추가 정보 기입이 되지 않은 경우 처리 - return SocialTokenResponse.builder() - .isWelcomeMember(true) - .token(createToken(identifier.get())) - .build(); + // 소셜 계정의 회원가입 처리 + MemberResponse welcomeMemberResponse = memberService.createMember(MemberCreateRequest.builder() + .nickname(resources.getNickname()) + .email(resources.getEmail()) + .socialId(resources.getSocialId()) + .accountType(accountType) + .profileImageUrl(resources.getProfileImageUrl()) + .build()); - } else { - // 소셜 계정이고 추가 정보도 기입된 경우 처리 - return SocialTokenResponse.builder() - .isWelcomeMember(false) - .token(createToken(identifier.get())) - .build(); - } + return SocialTokenResponse.builder() + .isWelcomeMember(true) + .token(createToken(welcomeMemberResponse)) + .build(); } @Transactional diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/CurationController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/CurationController.java index c25a4ab7..f411d948 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/CurationController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/CurationController.java @@ -1,17 +1,21 @@ package org.ktc2.cokaen.wouldyouin.curation.api; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.config.ParamDefaults; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.auth.Authorize; import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCreateRequest; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationEditRequest; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationResponse; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationSliceResponse; import org.ktc2.cokaen.wouldyouin.curation.application.CurationService; import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -30,32 +34,53 @@ public class CurationController { private final CurationService curationService; @GetMapping - public ApiResponseBody<List<CurationResponse>> getCurationByArea(@RequestParam Area area) { - return new ApiResponseBody<>(true, curationService.getAllByArea(area)); + public ResponseEntity<ApiResponseBody<CurationSliceResponse>> getCurationsByAreaOrderByCreatedDateDesc( + @RequestParam(defaultValue = ParamDefaults.AREA) Area area, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId + ) { + return ApiResponse.ok(curationService.getAllByAreaOrderByCreatedDateDesc( + area, PageRequest.of(page, size), lastId)); + } + + @GetMapping("/curators/{curatorId}") + public ResponseEntity<ApiResponseBody<CurationSliceResponse>> getCurationsByCuratorIdOrderByCreatedDateDesc( + @PathVariable("curatorId") Long curatorId, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId + ) { + return ApiResponse.ok(curationService.getAllByCuratorIdOrderByCreatedDateDesc( + curatorId, PageRequest.of(page, size), lastId)); } @GetMapping("/{curationId}") - public ApiResponseBody<CurationResponse> getCurationByCurationId( + public ResponseEntity<ApiResponseBody<CurationResponse>> getCurationByCurationId( @PathVariable("curationId") Long curationId) { - return new ApiResponseBody<>(true, curationService.getById(curationId)); + return ApiResponse.ok(curationService.getById(curationId)); } @PostMapping - public ApiResponseBody<CurationResponse> createCuration( + public ResponseEntity<ApiResponseBody<CurationResponse>> createCuration( @Valid @RequestBody CurationCreateRequest curationCreateRequest, @Authorize(MemberType.curator) MemberIdentifier curator) { - return new ApiResponseBody<>(true, curationService.create(curator.id(), curationCreateRequest)); + return ApiResponse.created(curationService.create(curator.id(), curationCreateRequest)); } @PutMapping("/{curationId}") - public ApiResponseBody<CurationResponse> updateCuration(@PathVariable Long curationId, - @Valid @RequestBody CurationEditRequest curationEditRequest) { - return new ApiResponseBody<>(true, curationService.update(curationId, curationEditRequest)); + public ResponseEntity<ApiResponseBody<CurationResponse>> updateCuration( + @PathVariable Long curationId, + @Valid @RequestBody CurationEditRequest curationEditRequest, + @Authorize(MemberType.curator) MemberIdentifier curator) { + return ApiResponse.ok(curationService.update(curator.id(), curationId, curationEditRequest)); } @DeleteMapping("/{curationId}") - public ApiResponseBody<Void> deleteCuration(@PathVariable Long curationId) { - curationService.delete(curationId); - return new ApiResponseBody<>(true, null); + public ResponseEntity<ApiResponseBody<Void>> deleteCuration( + @PathVariable Long curationId, + @Authorize(MemberType.curator) MemberIdentifier curator) { + curationService.delete(curator.id(), curationId); + return ApiResponse.noContent(); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCardRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCardRequest.java index 38241e5c..80946d11 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCardRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCardRequest.java @@ -1,16 +1,19 @@ package org.ktc2.cokaen.wouldyouin.curation.api.dto; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import java.util.List; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; @Getter @Builder(toBuilder = true) +@EqualsAndHashCode public class CurationCardRequest { @NotEmpty(message = "부제목은 필수입니다.") @@ -22,6 +25,11 @@ public class CurationCardRequest { private List<Long> imageIds; + @AssertTrue(message = "이미지는 최대 5개까지 등록할 수 있습니다.") + public boolean isImageSizeValid() { + return imageIds == null || imageIds.size() <= 5; + } + public CurationCard toEntity(List<CurationImage> images) { return CurationCard.builder() .subtitle(this.subtitle) diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCreateRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCreateRequest.java index 0b37f343..4e584fa6 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCreateRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationCreateRequest.java @@ -1,12 +1,14 @@ package org.ktc2.cokaen.wouldyouin.curation.api.dto; import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; import org.ktc2.cokaen.wouldyouin.event.persist.Event; @@ -14,6 +16,7 @@ @Getter @Builder(toBuilder = true) +@EqualsAndHashCode public class CurationCreateRequest { @NotEmpty(message = "제목은 필수입니다.") @@ -31,6 +34,14 @@ public class CurationCreateRequest { private List<Long> eventIds; + @AssertTrue(message = "큐레이션 카드의 개수는 1개 이상 10개 이하이어야 합니다.") + public boolean isCurationCardsSizeValid() { + if (this.curationCards == null) { + return false; + } + return 1 <= this.curationCards.size() && this.curationCards.size() <= 10; + } + public Curation toEntity(Curator curator, List<CurationCard> curationCards, List<Event> events) { return Curation.builder() .curator(curator) diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationEditRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationEditRequest.java index b8c56a32..2a691007 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationEditRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationEditRequest.java @@ -1,14 +1,18 @@ package org.ktc2.cokaen.wouldyouin.curation.api.dto; +import jakarta.validation.Valid; +import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import java.util.List; import lombok.Builder; +import lombok.EqualsAndHashCode; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; @Getter @Builder(toBuilder = true) +@EqualsAndHashCode public class CurationEditRequest { @NotEmpty(message = "제목은 필수입니다.") @@ -16,6 +20,7 @@ public class CurationEditRequest { private String content; + @Valid private List<CurationCardRequest> curationCards; @NotNull(message = "지역은 필수입니다.") @@ -24,4 +29,12 @@ public class CurationEditRequest { private List<String> hashTag; private List<Long> eventIds; + + @AssertTrue(message = "큐레이션 카드의 개수는 1개 이상 10개 이하이어야 합니다.") + public boolean isCurationCardsSizeValid() { + if (this.curationCards == null) { + return false; + } + return 1 <= this.curationCards.size() && this.curationCards.size() <= 10; + } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationResponse.java index 5d9db844..65489c61 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationResponse.java @@ -4,14 +4,10 @@ import java.util.List; import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; -import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; import org.ktc2.cokaen.wouldyouin.event.api.dto.relationResonse.CurationEventResponse; -import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.application.dto.relationResponse.CurationCuratorResponse; -import org.ktc2.cokaen.wouldyouin.member.persist.Curator; @Builder @Getter @@ -29,50 +25,17 @@ public class CurationResponse { private final LocalDateTime createdTime; public static CurationResponse from(Curation curation) { - Curator curator = curation.getCurator(); - List<CurationCard> curationCards = curation.getCurationCards(); - List<Event> events = curation.getEvents(); - return CurationResponse.builder() .id(curation.getId()) - .curator( - CurationCuratorResponse.builder() - .nickname(curator.getNickname()) - .email(curator.getEmail()) - .phone(curator.getPhone()) - .profileImageUrl(curator.getProfileImageUrl()) - .intro(curator.getIntro()) - .likes(curator.getLikes()) - .hashtags(curator.getHashTagList()) - .build() - ) + .curator(CurationCuratorResponse.from(curation.getCurator())) .title(curation.getTitle()) .content(curation.getContent()) - .curationCards( - curationCards.stream() - .map(card -> CurationCardResponse.builder() - .subtitle(card.getSubtitle()) - .content(card.getContent()) - .imageUrls(card.getCurationImages().stream().map(CurationImage::getUrl).toList()) - .build() - ).toList() - ) + .curationCards(curation.getCurationCards().stream() + .map(CurationCardResponse::from).toList()) .area(curation.getArea()) .hashTag(curation.getHashTag()) - .eventsInfo( - events.stream() - .map(event -> - CurationEventResponse.builder() - .id(event.getId()) - .title(event.getTitle()) - .location(event.getLocation()) - .startTime(event.getStartTime()) - .thumbnailImageUrl(event.getThumbnailUrl()) - .hostProfileImageUrl(event.getHost().getProfileImageUrl()) - .hostNickname(event.getHost().getNickname()) - .build() - ).toList() - ) + .eventsInfo(curation.getEvents().stream() + .map(CurationEventResponse::from).toList()) .createdTime(curation.getCreatedDate()) .modifiedDate(curation.getModifiedDate()) .build(); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationSliceResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationSliceResponse.java new file mode 100644 index 00000000..1e426d05 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/CurationSliceResponse.java @@ -0,0 +1,24 @@ +package org.ktc2.cokaen.wouldyouin.curation.api.dto; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import org.ktc2.cokaen.wouldyouin._common.api.SliceInfo; + +@Getter +@Builder +public class CurationSliceResponse { + + private final List<CurationResponse> curations; + private final SliceInfo slice; + + public static CurationSliceResponse of(List<CurationResponse> curationResponses, int sliceSize, long lastId) { + return CurationSliceResponse.builder() + .curations(curationResponses) + .slice(SliceInfo.builder() + .sliceSize(sliceSize) + .lastId(lastId) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/LocationFilter.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/LocationFilter.java new file mode 100644 index 00000000..aec28478 --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/api/dto/LocationFilter.java @@ -0,0 +1,18 @@ +package org.ktc2.cokaen.wouldyouin.curation.api.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +@Getter +@Setter +@NoArgsConstructor +@AllArgsConstructor +public class LocationFilter { + + private Double startLatitude = -90.0; + private Double startLongitude = -180.0; + private Double endLatitude = 90.0; + private Double endLongitude = 180.0; +} diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationCardService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationCardService.java index 34edfe9e..de13ad0e 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationCardService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationCardService.java @@ -5,8 +5,8 @@ import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.application.CurationImageService; import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; -import org.ktc2.cokaen.wouldyouin._common.exception.EntityParamIsNullException; import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; +import org.ktc2.cokaen.wouldyouin._common.exception.EntityParamIsNullException; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCardRequest; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCardResponse; import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; @@ -32,7 +32,7 @@ public CurationCardResponse getById(Long id) { @Transactional(readOnly = true) public List<CurationCardResponse> getByCurationId(Curation curation) { Optional.ofNullable(curation).orElseThrow(() -> new EntityParamIsNullException("Curation")); - return curationCardRepository.findAllByCuration(curation).stream() + return curationCardRepository.findAllByCurationOrderByIdAsc(curation).stream() .map(CurationCardResponse::from).toList(); } @@ -46,7 +46,7 @@ public void setCuration(CurationCard curationCard, Curation curation) { @Transactional public CurationCard create(CurationCardRequest request) { List<CurationImage> images = request.getImageIds().stream() - .map(curationImageService::getByIdOrThrow) + .map(curationImageService::getById) .toList(); CurationCard curationCard = curationCardRepository.save(request.toEntity(images)); images.forEach(image -> curationImageService.setCuration(image, curationCard)); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationService.java index 1c326fdf..162b1cf9 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/application/CurationService.java @@ -1,14 +1,14 @@ package org.ktc2.cokaen.wouldyouin.curation.application; import java.util.List; -import lombok.AccessLevel; import lombok.RequiredArgsConstructor; -import org.ktc2.cokaen.wouldyouin.Image.application.CurationImageService; import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.exception.UnauthorizedException; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCreateRequest; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationEditRequest; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationResponse; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationSliceResponse; import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; import org.ktc2.cokaen.wouldyouin.curation.persist.CurationCard; import org.ktc2.cokaen.wouldyouin.curation.persist.CurationRepository; @@ -16,11 +16,13 @@ import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.application.CuratorService; import org.ktc2.cokaen.wouldyouin.member.persist.Curator; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service -@RequiredArgsConstructor(access = AccessLevel.PUBLIC) +@RequiredArgsConstructor public class CurationService { private final CurationRepository curationRepository; @@ -39,9 +41,25 @@ public CurationResponse getById(Long curationId) { } @Transactional(readOnly = true) - public List<CurationResponse> getAllByArea(Area area) { - return curationRepository.findByArea(area).stream() - .map(CurationResponse::from).toList(); + public CurationSliceResponse getAllByAreaOrderByCreatedDateDesc(Area area, Pageable pageable, Long lastId) { + return getCurationSliceResponse( + curationRepository.findAllByAreaOrderByCreatedDateDesc(area, lastId, pageable), lastId); + } + + @Transactional(readOnly = true) + public CurationSliceResponse getAllByCuratorIdOrderByCreatedDateDesc(Long curatorId, Pageable pageable, Long lastId) { + return getCurationSliceResponse( + curationRepository.findAllByCuratorOrderByCreatedDateDesc( + curatorService.getByIdOrThrow(curatorId), lastId, pageable), lastId); + } + + private CurationSliceResponse getCurationSliceResponse(Slice<Curation> curationSlice, Long lastId) { + List<CurationResponse> curations = curationSlice.stream().map(CurationResponse::from).toList(); + if (!curationSlice.hasContent()) { + Long id = curationSlice.getContent().getLast().getId(); + return CurationSliceResponse.of(curations, curationSlice.getSize(), id); + } + return CurationSliceResponse.of(curations, curationSlice.getSize(), lastId); } @Transactional @@ -58,24 +76,34 @@ public CurationResponse create(Long curatorId, CurationCreateRequest curationCre return CurationResponse.from(curation); } + public void validateCuratorId(Long curatorId, Curation curation) { + if (!curatorId.equals(curation.getCurator().getId())) { + throw new UnauthorizedException("큐레이터 ID가 큐레이션의 큐레이터 ID와 일치하지 않습니다."); + } + } + @Transactional - public CurationResponse update(Long curationId, CurationEditRequest curationEditRequest) { - Curation target = getByIdOrThrow(curationId); + public CurationResponse update(Long curatorId, Long curationId, CurationEditRequest curationEditRequest) { + Curation curation = getByIdOrThrow(curationId); + validateCuratorId(curatorId, curation); List<CurationCard> curationCards = curationEditRequest.getCurationCards().stream() .map(curationCardService::create) .toList(); List<Event> events = curationEditRequest.getEventIds().stream() .map(eventService::getByIdOrThrow) .toList(); - target.getCurationCards().forEach(card -> curationCardService.delete(card.getId())); - target.updateFrom(curationEditRequest, curationCards, events); - curationCards.forEach(curationCard -> curationCardService.setCuration(curationCard, target)); - return CurationResponse.from(target); + curation.getCurationCards().forEach(card -> curationCardService.delete(card.getId())); + curation.updateFrom(curationEditRequest, curationCards, events); + curationCards.forEach(curationCard -> curationCardService.setCuration(curationCard, curation)); + return CurationResponse.from(curation); } @Transactional - public void delete(Long curationId) { - getByIdOrThrow(curationId); + public void delete(Long curatorId, Long curationId) { + Curation curation = getByIdOrThrow(curationId); + validateCuratorId(curatorId, curation); + curation.getCurationCards() + .forEach(curationCard -> curationCardService.delete(curationCard.getId())); curationRepository.deleteById(curationId); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/Curation.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/Curation.java index d9e61027..90462f05 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/Curation.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/Curation.java @@ -1,12 +1,12 @@ package org.ktc2.cokaen.wouldyouin.curation.persist; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,6 +17,7 @@ import jakarta.persistence.OneToMany; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Optional; import lombok.AccessLevel; @@ -25,7 +26,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.ktc2.cokaen.wouldyouin._common.converter.HashtagConverter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationEditRequest; import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.persist.Curator; @@ -46,7 +47,7 @@ public class Curation { private Long id; @JoinColumn(name = "curator_id") - @ManyToOne(cascade = CascadeType.ALL) + @ManyToOne(fetch = FetchType.LAZY) private Curator curator; @NotNull @@ -57,8 +58,8 @@ public class Curation { @Column(name = "content") private String content; - @OneToMany(mappedBy = "curation") - private List<CurationCard> curationCards; + @OneToMany(mappedBy = "curation", fetch = FetchType.LAZY) + private List<CurationCard> curationCards = new ArrayList<>(); @NotNull @Enumerated(EnumType.STRING) @@ -67,15 +68,15 @@ public class Curation { @Column(name = "hashtag") @Convert(converter = HashtagConverter.class) - private List<String> hashTag; + private List<String> hashTag = new ArrayList<>(); - @ManyToMany + @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "curation_event_relation", joinColumns = @JoinColumn(name = "curation_id"), inverseJoinColumns = @JoinColumn(name = "event_id") ) - private List<Event> events; + private List<Event> events = new ArrayList<>(); @CreatedDate @Column(name = "created_date") @@ -86,7 +87,8 @@ public class Curation { private LocalDateTime modifiedDate; @Builder - public Curation(Curator curator, String title, String content, List<CurationCard> curationCards, Area area, List<String> hashTag, List<Event> events) { + public Curation(Curator curator, String title, String content, List<CurationCard> curationCards, Area area, List<String> hashTag, + List<Event> events) { this.curator = curator; this.title = title; this.content = content; @@ -104,4 +106,4 @@ public void updateFrom(CurationEditRequest curationEditRequest, List<CurationCar Optional.ofNullable(curationCards).ifPresent(this::setCurationCards); Optional.ofNullable(events).ifPresent(this::setEvents); } -} +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCard.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCard.java index 5d9f6d2f..ba78a2d7 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCard.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCard.java @@ -2,12 +2,14 @@ import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; import lombok.Builder; @@ -33,12 +35,12 @@ public class CurationCard { @Column(name = "content") private String content; - @ManyToOne + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "curation_id") private Curation curation; - @OneToMany(mappedBy = "curationCard") - private List<CurationImage> curationImages; + @OneToMany(mappedBy = "curationCard", fetch = FetchType.LAZY) + private List<CurationImage> curationImages = new ArrayList<>(); @Builder public CurationCard(String subtitle, String content, Curation curation, List<CurationImage> images) { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCardRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCardRepository.java index e52bcd16..d312e320 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCardRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationCardRepository.java @@ -2,9 +2,12 @@ import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface CurationCardRepository extends JpaRepository<CurationCard, Long> { - List<CurationCard> findAllByCuration(Curation curation); + + @Query("SELECT CC FROM CurationCard CC JOIN FETCH CC.curation WHERE CC.curation = :curation ORDER BY CC.id ASC") + List<CurationCard> findAllByCurationOrderByIdAsc(Curation curation); } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationRepository.java index c5e1b35b..95ce7854 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/curation/persist/CurationRepository.java @@ -1,12 +1,23 @@ package org.ktc2.cokaen.wouldyouin.curation.persist; -import java.util.List; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin.member.persist.Curator; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface CurationRepository extends JpaRepository<Curation, Long> { - public List<Curation> findByArea(Area area); -} + @Query("SELECT C FROM Curation C JOIN FETCH C.curator " + + "WHERE C.area = :area AND C.id > :lastId " + + "ORDER BY C.id DESC") + Slice<Curation> findAllByAreaOrderByCreatedDateDesc(Area area, Long lastId, Pageable pageable); + + @Query("SELECT C FROM Curation C JOIN FETCH C.curator " + + "WHERE C.curator = :curator AND C.id > :lastId " + + "ORDER BY C.id DESC") + Slice<Curation> findAllByCuratorOrderByCreatedDateDesc(Curator curator, Long lastId, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/EventController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/EventController.java index 37861d31..abf24bd9 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/EventController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/EventController.java @@ -1,22 +1,33 @@ package org.ktc2.cokaen.wouldyouin.event.api; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; +import org.ktc2.cokaen.wouldyouin._common.config.ParamDefaults; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; +import org.ktc2.cokaen.wouldyouin.auth.Authorize; +import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.LocationFilter; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventResponse; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventSliceResponse; import org.ktc2.cokaen.wouldyouin.event.application.EventService; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; 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; @RestController @@ -27,14 +38,29 @@ public class EventController { private final EventService eventService; @GetMapping - public ResponseEntity<ApiResponseBody<List<EventResponse>>> getEvents() { - return ApiResponse.ok(eventService.getAll()); + public ResponseEntity<ApiResponseBody<EventSliceResponse>> getEventsByFilterOrderByDistanceAsc( + @ModelAttribute LocationFilter locationFilter, + @ModelAttribute Location currentLocation, + @RequestParam(defaultValue = ParamDefaults.TITLE) String title, + @RequestParam(defaultValue = ParamDefaults.CATEGORY) Category category, + @RequestParam(defaultValue = ParamDefaults.AREA) Area area, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId + ) { + return ApiResponse.ok(eventService.getAllByFilterOrderByDistanceAsc( + locationFilter, currentLocation, title, category, area, PageRequest.of(page, size), lastId)); } @GetMapping("/hosts/{hostId}") - public ResponseEntity<ApiResponseBody<List<EventResponse>>> getEventsByHostId( - @PathVariable Long hostId) { - return ApiResponse.ok(eventService.getAllByHostId(hostId)); + public ResponseEntity<ApiResponseBody<EventSliceResponse>> getEventsByHostId( + @PathVariable Long hostId, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId + ) { + return ApiResponse.ok(eventService.getAllByHostIdOrderByCreatedDateDesc( + hostId, PageRequest.of(page, size), lastId)); } @GetMapping("/{eventId}") @@ -45,20 +71,23 @@ public ResponseEntity<ApiResponseBody<EventResponse>> getEventByEventId( @PostMapping public ResponseEntity<ApiResponseBody<EventResponse>> createEvent( - @Valid @RequestBody EventCreateRequest eventCreateRequest) { - return ApiResponse.created(eventService.create(eventCreateRequest)); + @Valid @RequestBody EventCreateRequest eventCreateRequest, + @Authorize(MemberType.host) MemberIdentifier host) { + return ApiResponse.created(eventService.create(host.id(), eventCreateRequest)); } @PutMapping("/{eventId}") public ResponseEntity<ApiResponseBody<EventResponse>> updateEvent(@PathVariable Long eventId, - @Valid @RequestBody EventEditRequest eventEditRequest) { - return ApiResponse.ok(eventService.update(eventId, eventEditRequest)); + @Valid @RequestBody EventEditRequest eventEditRequest, + @Authorize(MemberType.host) MemberIdentifier host) { + return ApiResponse.ok(eventService.update(host.id(), eventId, eventEditRequest)); } @DeleteMapping("/{eventId}") public ResponseEntity<ApiResponseBody<Void>> deleteEvent( - @PathVariable("eventId") Long eventId) { - eventService.delete(eventId); + @PathVariable("eventId") Long eventId, + @Authorize(MemberType.host) MemberIdentifier host) { + eventService.delete(host.id(), eventId); return ApiResponse.noContent(); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventCreateRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventCreateRequest.java index 5dc6884a..e276a6e2 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventCreateRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventCreateRequest.java @@ -2,6 +2,8 @@ import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -10,9 +12,9 @@ import lombok.Builder; import lombok.Getter; import org.ktc2.cokaen.wouldyouin.Image.persist.EventImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; -import org.ktc2.cokaen.wouldyouin._common.persist.Category; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.persist.Host; @@ -20,9 +22,6 @@ @Builder public class EventCreateRequest { - @NotNull(message = "hostId는 필수입니다.") - private Long hostId; - @NotBlank(message = "제목은 필수입니다.") private String title; @@ -43,10 +42,12 @@ public class EventCreateRequest { private LocalDateTime endTime; @NotNull(message = "가격은 필수입니다.") - @Size(min = 0, max = 1000000, message = "가격은 0원 이상 1,000,000원 이하입니다.") + @Min(value = 0, message = "가격은 0원 이상입니다.") + @Max(value = 1000000, message = "가격은 1,000,000원 이하입니다.") private Integer price; - @Size(min = 0, max = 1000, message = "총 좌석은 0석 이상 1,000석 이하입니다.") + @Min(value = 0, message = "총 좌석은 0석 이상입니다.") + @Max(value = 1000, message = "총 좌석은 1,000석 이하입니다.") private Integer totalSeat; @NotNull(message = "카테고리는 필수입니다.") @@ -64,7 +65,7 @@ public boolean isEndTimeAfterStartTime() { @AssertTrue(message = "이미지는 최대 5개까지 등록할 수 있습니다.") public boolean isImageSizeValid() { - return imageIds.size() <= 5; + return imageIds == null || imageIds.size() <= 5; } public Event toEntity(Host host, List<EventImage> images) { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventEditRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventEditRequest.java index 6e307edd..f46afcab 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventEditRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventEditRequest.java @@ -2,6 +2,8 @@ import jakarta.validation.constraints.AssertTrue; import jakarta.validation.constraints.FutureOrPresent; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Size; @@ -9,9 +11,9 @@ import java.util.List; import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; -import org.ktc2.cokaen.wouldyouin._common.persist.Category; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; import org.ktc2.cokaen.wouldyouin.event.persist.Event; @Getter @@ -38,10 +40,12 @@ public class EventEditRequest { private LocalDateTime endTime; @NotNull(message = "가격은 필수입니다.") - @Size(min = 0, max = 1000000, message = "가격은 0원 이상 1,000,000원 이하입니다.") + @Min(value = 0, message = "가격은 0원 이상입니다.") + @Max(value = 1000000, message = "가격은 1,000,000원 이하입니다.") private Integer price; - @Size(min = 0, max = 1000, message = "총 좌석은 0석 이상 1,000석 이하입니다.") + @Min(value = 0, message = "총 좌석은 0석 이상입니다.") + @Max(value = 1000, message = "총 좌석은 1,000석 이하입니다.") private Integer totalSeat; @NotNull(message = "카테고리는 필수입니다.") diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventResponse.java index b2b6b829..7ff20f85 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventResponse.java @@ -3,9 +3,9 @@ import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; -import org.ktc2.cokaen.wouldyouin._common.persist.Category; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.application.dto.relation.EventHostResponse; import org.ktc2.cokaen.wouldyouin.member.persist.Host; @@ -34,15 +34,7 @@ public static EventResponse from(Event event) { .id(event.getId()) .title(event.getTitle()) .content(event.getContent()) - .host(EventHostResponse.builder() - .nickname(host.getNickname()) - .email(host.getEmail()) - .phone(host.getPhone()) - .profileImageUrl(host.getProfileImage().getUrl()) - .intro(host.getIntro()) - .likes(host.getLikes()) - .hashtags(host.getHashTagList()) - .build()) + .host(EventHostResponse.from(host)) .area(event.getArea()) .location(event.getLocation()) .startTime(event.getStartTime()) diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventSliceResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventSliceResponse.java new file mode 100644 index 00000000..51889b9a --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/EventSliceResponse.java @@ -0,0 +1,24 @@ +package org.ktc2.cokaen.wouldyouin.event.api.dto; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import org.ktc2.cokaen.wouldyouin._common.api.SliceInfo; + +@Getter +@Builder +public class EventSliceResponse { + + private List<EventResponse> events; + private SliceInfo slice; + + public static EventSliceResponse of(List<EventResponse> eventResponses, int sliceSize, long lastId) { + return EventSliceResponse.builder() + .events(eventResponses) + .slice(SliceInfo.builder() + .sliceSize(sliceSize) + .lastId(lastId) + .build()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/relationResonse/CurationEventResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/relationResonse/CurationEventResponse.java index 92073cce..6073a499 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/relationResonse/CurationEventResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/api/dto/relationResonse/CurationEventResponse.java @@ -3,7 +3,8 @@ import java.time.LocalDateTime; import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; +import org.ktc2.cokaen.wouldyouin.event.persist.Event; @Getter @Builder @@ -16,4 +17,16 @@ public class CurationEventResponse { private String thumbnailImageUrl; private String hostProfileImageUrl; private String hostNickname; + + public static CurationEventResponse from(Event event) { + return CurationEventResponse.builder() + .id(event.getId()) + .title(event.getTitle()) + .location(event.getLocation()) + .startTime(event.getStartTime()) + .thumbnailImageUrl(event.getThumbnailUrl()) + .hostProfileImageUrl(event.getHost().getProfileImageUrl()) + .hostNickname(event.getHost().getNickname()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/application/EventService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/application/EventService.java index c2918190..8a31d149 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/application/EventService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/application/EventService.java @@ -3,13 +3,22 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.application.EventImageService; +import org.ktc2.cokaen.wouldyouin._common.exception.CurrentLocationEmptyException; import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; +import org.ktc2.cokaen.wouldyouin._common.exception.UnauthorizedException; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.LocationFilter; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventResponse; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventSliceResponse; import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.event.persist.EventRepository; import org.ktc2.cokaen.wouldyouin.member.application.HostService; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -22,13 +31,34 @@ public class EventService { private final EventImageService eventImageService; @Transactional(readOnly = true) - public List<EventResponse> getAll() { - return eventRepository.findAll().stream().map(EventResponse::from).toList(); + public EventSliceResponse getAllByFilterOrderByDistanceAsc(LocationFilter location, Location currentLocation, String title, + Category category, Area area, Pageable pageable, Long lastId) { + validateCurrentLocation(currentLocation); + return getEventSliceResponse( + eventRepository.findAllByFilterOrderByDistance(location.getStartLatitude(), location.getStartLongitude(), + location.getEndLatitude(), location.getEndLongitude(), currentLocation.getLatitude(), currentLocation.getLongitude(), + title, category, area, pageable), lastId); + } + + private void validateCurrentLocation(Location currentLocation) { + if (currentLocation.getLatitude() == null || currentLocation.getLongitude() == null) { + throw new CurrentLocationEmptyException(); + } } @Transactional(readOnly = true) - public List<EventResponse> getAllByHostId(Long hostId) { - return eventRepository.findByHostId(hostId).stream().map(EventResponse::from).toList(); + public EventSliceResponse getAllByHostIdOrderByCreatedDateDesc(Long hostId, Pageable pageable, Long lastId) { + return getEventSliceResponse( + eventRepository.findAllByHostIdOrderByEventIdDesc(hostId, lastId, pageable), lastId); + } + + private EventSliceResponse getEventSliceResponse(Slice<Event> eventSlice, Long lastId) { + List<EventResponse> events = eventSlice.stream().map(EventResponse::from).toList(); + if (eventSlice.hasContent()) { + Long id = eventSlice.getContent().getLast().getId(); + return EventSliceResponse.of(events, eventSlice.getSize(), id); + } + return EventSliceResponse.of(events, eventSlice.getSize(), lastId); } @Transactional(readOnly = true) @@ -41,31 +71,40 @@ public Event getByIdOrThrow(Long id) throws EntityNotFoundException { } @Transactional - public EventResponse create(EventCreateRequest eventCreateRequest) { - return EventResponse.from(eventRepository.save(eventCreateRequest.toEntity( - hostService.getByIdOrThrow(eventCreateRequest.getHostId()), + public EventResponse create(Long hostId, EventCreateRequest eventCreateRequest) { + return EventResponse.from( + eventRepository.save(eventCreateRequest.toEntity( + hostService.getByIdOrThrow(hostId), eventCreateRequest.getImageIds().stream() - .map(eventImageService::getByIdOrThrow) + .map(eventImageService::getById) .toList()))); } + private void validateHostId(Long hostId, Event event) { + if (!hostId.equals(event.getHost().getId())) { + throw new UnauthorizedException("호스트 ID가 행사의 호스트 ID와 일치하지 않습니다."); + } + } + @Transactional - public EventResponse update(Long id, EventEditRequest eventEditRequest) { - Event target = getByIdOrThrow(id); - target.updateFrom(eventEditRequest, eventEditRequest.getImageIds().stream() - .map(eventImageService::getByIdOrThrow) + public EventResponse update(Long hostId, Long eventId, EventEditRequest eventEditRequest) { + Event event = getByIdOrThrow(eventId); + validateHostId(hostId, event); + event.updateFrom(eventEditRequest, eventEditRequest.getImageIds().stream() + .map(eventImageService::getById) .toList()); - return EventResponse.from(target); + return EventResponse.from(event); } @Transactional - public void decreaseLeftSeat(Long id, Integer count) { - getByIdOrThrow(id).decreaseLeftSeat(count); + public void delete(Long hostId, Long eventId) { + Event event = getByIdOrThrow(eventId); + validateHostId(hostId, event); + eventRepository.deleteById(eventId); } @Transactional - public void delete(Long id) { - getByIdOrThrow(id); - eventRepository.deleteById(id); + public void decreaseLeftSeat(Long eventId, Integer count) { + getByIdOrThrow(eventId).decreaseLeftSeat(count); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/Event.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/Event.java index cbc2c269..98efe2a3 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/Event.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/Event.java @@ -1,12 +1,12 @@ package org.ktc2.cokaen.wouldyouin.event.persist; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.EntityListeners; import jakarta.persistence.EnumType; import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -17,6 +17,7 @@ import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotNull; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; import lombok.Builder; @@ -25,9 +26,9 @@ import lombok.Setter; import org.ktc2.cokaen.wouldyouin.Image.persist.EventImage; import org.ktc2.cokaen.wouldyouin._common.exception.NoLeftSeatException; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; -import org.ktc2.cokaen.wouldyouin._common.persist.Category; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; import org.ktc2.cokaen.wouldyouin.member.persist.Host; import org.springframework.data.annotation.CreatedDate; @@ -54,7 +55,7 @@ public class Event { private String content; @NotNull - @ManyToOne(cascade = CascadeType.ALL) + @ManyToOne @JoinColumn(name = "host_id") private Host host; @@ -92,11 +93,12 @@ public class Event { @Enumerated(EnumType.STRING) private Category category; + // Todo: 이미지를 사용하는 모든 엔티티 thumnail 설정 @Column(name = "thumbnail_url") private String thumbnailUrl; - @OneToMany(mappedBy = "event") - private List<EventImage> images; + @OneToMany(mappedBy = "event", fetch = FetchType.LAZY) + private List<EventImage> images = new ArrayList<>(); @CreatedDate @Column(name = "created_date") diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/EventRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/EventRepository.java index 4b7e349d..837f5463 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/EventRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/event/persist/EventRepository.java @@ -1,11 +1,31 @@ package org.ktc2.cokaen.wouldyouin.event.persist; -import java.util.List; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface EventRepository extends JpaRepository<Event, Long> { - public List<Event> findByHostId(Long hostId); + @Query("SELECT E FROM Event E JOIN FETCH E.host " + + "WHERE E.host.Id = :hostId " + + "AND E.host.Id > :lastId " + + "ORDER BY E.id DESC") + Slice<Event> findAllByHostIdOrderByEventIdDesc(Long hostId, Long lastId, Pageable pageable); + + @Query("SELECT E FROM Event E JOIN FETCH E.host " + + "WHERE (E.location.latitude between :startLatitude AND :endLatitude) " + + "AND (E.location.longitude between :startLongitude AND :endLongitude) " + + "AND (E.title LIKE %:title%) " + + "AND (:category = '전체' OR E.category = :category) " + + "AND (:area = '전체' OR E.area = :area) " + + "ORDER BY (:currentLatitude - E.location.latitude) * (:currentLatitude - E.location.latitude) + " + + "(:currentLongitude - E.location.longitude) * (:currentLongitude - E.location.longitude) ASC") + Slice<Event> findAllByFilterOrderByDistance( + Double startLatitude, Double startLongitude, Double endLatitude, Double endLongitude, + Double currentLatitude, Double currentLongitude, String title, Category category, Area area, Pageable pageable); } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/like/api/LikeController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/like/api/LikeController.java index 9f4cba09..02a434bc 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/like/api/LikeController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/like/api/LikeController.java @@ -1,10 +1,12 @@ package org.ktc2.cokaen.wouldyouin.like.api; +import java.util.List; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; import org.ktc2.cokaen.wouldyouin.auth.Authorize; import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; +import org.ktc2.cokaen.wouldyouin.like.application.LikeResponse; import org.ktc2.cokaen.wouldyouin.like.application.LikeServiceFactory; import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.springframework.http.HttpStatus; @@ -24,19 +26,22 @@ public class LikeController { private final LikeServiceFactory likeServiceFactory; + // Todo: 와일드카드 수정 + // Todo: api 요청시 create, delete를 분리하지말고 토글방식으로 하면 어떨지 + @GetMapping - public ResponseEntity<?> getLikes(@Authorize(MemberType.normal) MemberIdentifier identifier, @RequestParam("type") MemberType memberType) { + public ResponseEntity<ApiResponseBody<List<LikeResponse>>> getLikes(@Authorize(MemberType.normal) MemberIdentifier identifier, @RequestParam("type") MemberType memberType) { return ApiResponse.ok(likeServiceFactory.getLikeServiceFrom(memberType).getLikes(identifier.id())); } @PostMapping("/{targetMemberId}") - public ResponseEntity<?> createLike(@Authorize(MemberType.normal) MemberIdentifier identifier, @PathVariable("targetMemberId") Long targetId) { + public ResponseEntity<ApiResponseBody<LikeResponse>> createLike(@Authorize(MemberType.normal) MemberIdentifier identifier, @PathVariable("targetMemberId") Long targetId) { return ApiResponse.created(likeServiceFactory.getLikeServiceFrom(targetId).create(identifier.id(), targetId)); } @DeleteMapping("/{targetMemberId}") - public ResponseEntity<?> deleteLike(@Authorize(MemberType.normal) MemberIdentifier identifier, @PathVariable("targetMemberId") Long targetId) { + public ResponseEntity<ApiResponseBody<Void>> deleteLike(@Authorize(MemberType.normal) MemberIdentifier identifier, @PathVariable("targetMemberId") Long targetId) { likeServiceFactory.getLikeServiceFrom(targetId).delete(identifier.id(), targetId); - return ResponseEntity.status(HttpStatus.NO_CONTENT).body(new ApiResponseBody<>(true, null)); + return ApiResponse.noContent(); } } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/like/persist/LikeRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/like/persist/LikeRepository.java index b1a3e946..3bb61ce8 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/like/persist/LikeRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/like/persist/LikeRepository.java @@ -13,4 +13,3 @@ public interface LikeRepository <LikeType extends Like<? extends LikeableMember> Optional<LikeType> findByMemberAndLikeableMember(Member member, LikeableMember likeableMember); List<LikeType> findAllByMember(Member member); } - diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorService.java index c4ff8ad6..c98a6f34 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorService.java @@ -1,6 +1,5 @@ package org.ktc2.cokaen.wouldyouin.member.application; -import java.util.List; import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; @@ -62,7 +61,7 @@ public MemberResponse updateCurator(Long curatorId, CuratorEditRequest request) Optional.ofNullable(request.getArea()).ifPresent(curator::setArea); Optional.ofNullable(request.getIntro()).ifPresent(curator::setIntro); Optional.ofNullable(request.getProfileImageId()) - .map(memberImageService::getByIdOrThrow) + .map(memberImageService::getById) .ifPresent(curator::setProfileImage); return MemberResponse.from(curator); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/HostService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/HostService.java index e66e5074..f2cceed8 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/HostService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/HostService.java @@ -26,9 +26,9 @@ public class HostService implements MemberServiceCommonBehavior, LikeableMemberS @Transactional public MemberResponse createHost(HostCreateRequest request) { String hashedPassword = passwordEncoder.encode(request.getPassword()); - MemberImage profileImage = memberImageService.getByIdOrThrow(request.getProfileImageId()); + MemberImage profileImage = memberImageService.getById(request.getProfileImageId()); Host createdHost = hostRepository.save(request.toEntity(hashedPassword, profileImage)); - profileImage.setBaseMember(createdHost); + memberImageService.setBaseMember(profileImage, createdHost); return MemberResponse.from(createdHost); } @@ -37,7 +37,7 @@ public MemberResponse updateHost(Long hostId, HostEditRequest request) { Host host = getByIdOrThrow(hostId); Optional.ofNullable(request.getNickname()).ifPresent(host::setNickname); Optional.ofNullable(request.getPhoneNumber()).ifPresent(host::setPhone); - Optional.ofNullable(request.getProfileImageId()).map(memberImageService::getByIdOrThrow).ifPresent(host::setProfileImage); + Optional.ofNullable(request.getProfileImageId()).map(memberImageService::getById).ifPresent(host::setProfileImage); Optional.ofNullable(request.getIntro()).ifPresent(host::setIntro); Optional.ofNullable(request.getHashtag()).ifPresent(host::setHashtag); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/MemberService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/MemberService.java index 87a06847..ea657969 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/MemberService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/MemberService.java @@ -3,6 +3,7 @@ import java.util.Optional; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; +import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; import org.ktc2.cokaen.wouldyouin.member.application.dto.MemberResponse; import org.ktc2.cokaen.wouldyouin.member.application.dto.request.MemberAdditionalInfoRequest; @@ -23,7 +24,8 @@ public class MemberService implements MemberServiceCommonBehavior { @Transactional public MemberResponse createMember(MemberCreateRequest request) { - return MemberResponse.from(memberRepository.save(request.toEntity())); + MemberImage profileImage = memberImageService.convert(request.getProfileImageUrl()); + return MemberResponse.from(memberRepository.save(request.toEntity(profileImage))); } @Transactional @@ -32,7 +34,9 @@ public MemberResponse updateMember(Long memberId, MemberEditRequest editRequest) Optional.ofNullable(editRequest.getNickname()).ifPresent(member::setNickname); Optional.ofNullable(editRequest.getArea()).ifPresent(member::setArea); Optional.ofNullable(editRequest.getPhoneNumber()).ifPresent(member::setPhone); - Optional.ofNullable(editRequest.getProfileImageId()).map(memberImageService::getByIdOrThrow).ifPresent(member::setProfileImage); + Optional.ofNullable(editRequest.getProfileImageId()) + .map(memberImageService::getById) + .ifPresent(member::setProfileImage); return MemberResponse.from(member); } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/MemberResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/MemberResponse.java index 26968067..ed56592b 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/MemberResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/MemberResponse.java @@ -2,7 +2,7 @@ import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.ktc2.cokaen.wouldyouin.member.persist.BaseMember; import org.ktc2.cokaen.wouldyouin.member.persist.Curator; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relation/EventHostResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relation/EventHostResponse.java index 82ae4772..2430622f 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relation/EventHostResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relation/EventHostResponse.java @@ -3,6 +3,7 @@ import java.util.List; import lombok.Builder; import lombok.Getter; +import org.ktc2.cokaen.wouldyouin.member.persist.Host; @Builder @Getter @@ -15,4 +16,16 @@ public class EventHostResponse { private String intro; private Integer likes; private List<String> hashtags; + + public static EventHostResponse from(Host host) { + return EventHostResponse.builder() + .nickname(host.getNickname()) + .email(host.getEmail()) + .phone(host.getPhone()) + .profileImageUrl(host.getProfileImage().getUrl()) + .intro(host.getIntro()) + .likes(host.getLikes()) + .hashtags(host.getHashTagList()) + .build(); + } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relationResponse/CurationCuratorResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relationResponse/CurationCuratorResponse.java index 118cb984..2516823c 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relationResponse/CurationCuratorResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/relationResponse/CurationCuratorResponse.java @@ -3,6 +3,7 @@ import java.util.List; import lombok.Builder; import lombok.Getter; +import org.ktc2.cokaen.wouldyouin.member.persist.Curator; @Builder @Getter @@ -15,4 +16,17 @@ public class CurationCuratorResponse { private String intro; private Integer likes; private List<String> hashtags; + + public static CurationCuratorResponse from(Curator curator) { + return + CurationCuratorResponse.builder() + .nickname(curator.getNickname()) + .email(curator.getEmail()) + .phone(curator.getPhone()) + .profileImageUrl(curator.getProfileImageUrl()) + .intro(curator.getIntro()) + .likes(curator.getLikes()) + .hashtags(curator.getHashTagList()) + .build(); + } } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/MemberAdditionalInfoRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/MemberAdditionalInfoRequest.java index 4ec5a27c..64bfff22 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/MemberAdditionalInfoRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/MemberAdditionalInfoRequest.java @@ -2,7 +2,7 @@ import lombok.Getter; import lombok.RequiredArgsConstructor; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; @Getter @RequiredArgsConstructor diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/create/MemberCreateRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/create/MemberCreateRequest.java index a1467217..f021e163 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/create/MemberCreateRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/create/MemberCreateRequest.java @@ -1,11 +1,10 @@ package org.ktc2.cokaen.wouldyouin.member.application.dto.request.create; -import java.util.List; import lombok.Builder; import lombok.Getter; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.member.persist.AccountType; import org.ktc2.cokaen.wouldyouin.member.persist.Member; @@ -15,17 +14,17 @@ public class MemberCreateRequest extends MemberCreateRequestBase { protected AccountType accountType; protected String socialId; - protected MemberImage profileImage; + protected String profileImageUrl; @Builder - protected MemberCreateRequest(String nickname, String email, AccountType accountType, String socialId, MemberImage profileImage) { + protected MemberCreateRequest(String nickname, String email, AccountType accountType, String socialId, String profileImageUrl) { super(nickname, email); this.accountType = accountType; this.socialId = socialId; - this.profileImage = profileImage; + this.profileImageUrl = profileImageUrl; } - public Member toEntity() { + public Member toEntity(MemberImage profileImage) { return Member.builder() .nickname(this.nickname) .email(this.email) @@ -34,7 +33,7 @@ public Member toEntity() { .socialId(this.socialId) .area(Area.서울) .gender("") - .profileImage(this.profileImage) + .profileImage(profileImage) .build(); } } diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/CuratorEditRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/CuratorEditRequest.java index bc89596e..bef7ce24 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/CuratorEditRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/CuratorEditRequest.java @@ -1,14 +1,16 @@ package org.ktc2.cokaen.wouldyouin.member.application.dto.request.edit; import io.micrometer.common.lang.Nullable; +import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; @Getter public class CuratorEditRequest extends MemberEditRequest { @Nullable private final String intro; + @Builder(builderMethodName = "curatorEditRequestBuilder") public CuratorEditRequest(@Nullable String nickname, @Nullable String phoneNumber, @Nullable Long profileImageId, @Nullable Area area, @Nullable String intro) { super(nickname, phoneNumber, profileImageId, area); this.intro = intro; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/MemberEditRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/MemberEditRequest.java index 8d96cbfd..46e591a8 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/MemberEditRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/application/dto/request/edit/MemberEditRequest.java @@ -3,7 +3,7 @@ import jakarta.annotation.Nullable; import lombok.Builder; import lombok.Getter; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; @Getter public class MemberEditRequest extends MemberEditRequestBase { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/BaseMember.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/BaseMember.java index fccfd8c1..2fb3ad56 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/BaseMember.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/BaseMember.java @@ -5,6 +5,7 @@ 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; @@ -12,7 +13,6 @@ import jakarta.persistence.InheritanceType; import jakarta.persistence.OneToOne; import jakarta.persistence.PrimaryKeyJoinColumn; -import java.util.List; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Curator.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Curator.java index eafa22e4..25431476 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Curator.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Curator.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.OneToMany; import java.util.List; import lombok.AccessLevel; @@ -11,7 +12,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; @Getter @@ -30,11 +31,12 @@ public class Curator extends Member implements LikeableMember { @Column(nullable = false) private String hashtag; - @OneToMany(mappedBy = "curator") + @OneToMany(mappedBy = "curator", fetch = FetchType.LAZY) private List<Curation> curations; @Builder(builderMethodName = "curatorBuilder") - public Curator(AccountType accountType, String email, String nickname, String phone, MemberImage profileImage, Area area, String gender, String socialId) { + public Curator(AccountType accountType, String email, String nickname, String phone, MemberImage profileImage, Area area, String gender, + String socialId) { super(accountType, MemberType.curator, email, nickname, phone, profileImage, area, gender, socialId); this.intro = ""; this.likes = 0; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Host.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Host.java index 4edab6a7..1ef6efee 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Host.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Host.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.OneToMany; import java.util.List; import lombok.AccessLevel; @@ -32,7 +33,7 @@ public class Host extends BaseMember implements LikeableMember { @Column(nullable = false) private String hashtag; - @OneToMany(mappedBy = "host") + @OneToMany(mappedBy = "host", fetch = FetchType.LAZY) private List<Event> events; @Builder diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Member.java b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Member.java index 9e507e01..d5b4fe5c 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Member.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/member/persist/Member.java @@ -3,6 +3,7 @@ import jakarta.persistence.Column; import jakarta.persistence.DiscriminatorValue; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.OneToMany; import java.util.List; import lombok.AccessLevel; @@ -11,7 +12,7 @@ import lombok.NoArgsConstructor; import lombok.Setter; import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; import org.ktc2.cokaen.wouldyouin.like.persist.CuratorLike; import org.ktc2.cokaen.wouldyouin.like.persist.HostLike; import org.ktc2.cokaen.wouldyouin.member.application.dto.request.MemberAdditionalInfoRequest; @@ -38,16 +39,16 @@ public class Member extends BaseMember { @Column(length = 1000) private String refreshToken; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<CuratorLike> curatorLikes; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<HostLike> hostLikes; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<Reservation> reservations; - @OneToMany(mappedBy = "member") + @OneToMany(mappedBy = "member", fetch = FetchType.LAZY) private List<Review> reviews; // for Curator diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/payment/application/PaymentService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/payment/application/PaymentService.java index 81bc10c8..70cb9ca3 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/payment/application/PaymentService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/payment/application/PaymentService.java @@ -3,6 +3,7 @@ import java.net.URI; import java.util.HashMap; import java.util.Map; +import org.ktc2.cokaen.wouldyouin._common.exception.FailedToPayException; import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayRequest; import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayResponse; import org.springframework.beans.factory.annotation.Value; @@ -14,6 +15,7 @@ @Service public class PaymentService { + // Todo: util로 이동하여 재사용 private final RestClient client = RestClient.builder().build(); @Value("${oauth.payment.kakao_pay_request_host}") @@ -38,16 +40,16 @@ public KakaoPayResponse createPayment(KakaoPayRequest kakaoPayRequest) { try { return client.post() .uri(URI.create("https://" + kakaoPayRequestHost + kakaoPaySinglePaymentUrl)) - .headers(httpHeaders -> httpHeaders.addAll(createKakaoPayReqeustHeaders())) + .headers(httpHeaders -> httpHeaders.addAll(createKakaoPayRequestHeaders())) .body(createKakaoPayRequestBody(kakaoPayRequest)) .retrieve() .body(KakaoPayResponse.class); } catch (Exception ex) { - throw new RuntimeException(); + throw new FailedToPayException("Kakao Pay"); } } - private HttpHeaders createKakaoPayReqeustHeaders() { + private HttpHeaders createKakaoPayRequestHeaders() { HttpHeaders headers = new HttpHeaders(); headers.add("Host", kakaoPayRequestHost); headers.add("Authorization", "SECRET_KEY " + secretKey); diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/payment/dto/KakaoPayResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/payment/dto/KakaoPayResponse.java index bc746112..b17b9053 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/payment/dto/KakaoPayResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/payment/dto/KakaoPayResponse.java @@ -1,26 +1,22 @@ package org.ktc2.cokaen.wouldyouin.payment.dto; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; +import com.fasterxml.jackson.databind.annotation.JsonNaming; import java.time.LocalDateTime; import lombok.AllArgsConstructor; import lombok.Getter; @Getter @AllArgsConstructor +@JsonNaming(value = SnakeCaseStrategy.class) public class KakaoPayResponse { - @JsonProperty("tid") private final String tid; - @JsonProperty("next_redirect_app_url") private final String nextRedirectAppUrl; - @JsonProperty("next_redirect_mobile_url") private final String nextRedirectMobileUrl; - @JsonProperty("next_redirect_pc_url") private final String nextRedirectPcUrl; - @JsonProperty("android_app_scheme") private final String androidAppScheme; - @JsonProperty("ios_app_scheme") private final String iosAppScheme; - @JsonProperty("created_at") private final LocalDateTime createdAt; } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/ReservationController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/ReservationController.java index d1b4c4c7..8bdc91ea 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/ReservationController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/ReservationController.java @@ -1,17 +1,19 @@ package org.ktc2.cokaen.wouldyouin.reservation.api; import jakarta.validation.Valid; -import java.util.List; import lombok.RequiredArgsConstructor; +import org.ktc2.cokaen.wouldyouin._common.api.ApiResponse; import org.ktc2.cokaen.wouldyouin._common.api.ApiResponseBody; +import org.ktc2.cokaen.wouldyouin._common.config.ParamDefaults; import org.ktc2.cokaen.wouldyouin.auth.Authorize; import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayResponse; import org.ktc2.cokaen.wouldyouin.reservation.application.ReservationService; -import org.ktc2.cokaen.wouldyouin.reservation.application.dto.ReservationRequest; -import org.ktc2.cokaen.wouldyouin.reservation.application.dto.ReservationResponse; -import org.springframework.http.HttpStatus; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationRequest; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationResponse; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationSliceResponse; +import org.springframework.data.domain.PageRequest; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -19,6 +21,7 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -29,44 +32,41 @@ public class ReservationController { private final ReservationService reservationService; @GetMapping - public ResponseEntity<ApiResponseBody<List<ReservationResponse>>> getReservations() { - return ResponseEntity.status(HttpStatus.OK) - .body(new ApiResponseBody<>(true, reservationService.getAll())); + public ResponseEntity<ApiResponseBody<ReservationSliceResponse>> getReservationsByMemberId( + @Authorize({MemberType.normal, MemberType.curator}) MemberIdentifier member, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId) { + return ApiResponse.ok(reservationService.getAllByMemberId(member.id(), PageRequest.of(page, size), lastId)); } - @GetMapping("/{reservationId}") - public ResponseEntity<ApiResponseBody<ReservationResponse>> getReservation( - @PathVariable Long reservationId) { - return ResponseEntity.status(HttpStatus.OK) - .body(new ApiResponseBody<>(true, reservationService.getById(reservationId))); - } - - @GetMapping("/members/{memberId}") - public ResponseEntity<ApiResponseBody<List<ReservationResponse>>> getReservationsByMemberId( - @PathVariable Long memberId) { - return ResponseEntity.status(HttpStatus.OK) - .body(new ApiResponseBody<>(true, reservationService.getAllByMemberId(memberId))); + @GetMapping("/events/{eventId}") + public ResponseEntity<ApiResponseBody<ReservationSliceResponse>> getReservationsByEventId( + @PathVariable Long eventId, + @RequestParam(defaultValue = ParamDefaults.PAGE) Integer page, + @RequestParam(defaultValue = ParamDefaults.PAGE_SIZE) Integer size, + @RequestParam(defaultValue = ParamDefaults.LAST_ID) Long lastId) { + return ApiResponse.ok(reservationService.getAllByEventId(eventId, PageRequest.of(page, size), lastId)); } - @GetMapping("/events/{eventId}") - public ResponseEntity<ApiResponseBody<List<ReservationResponse>>> getReservationsByEventId( - @PathVariable Long eventId) { - return ResponseEntity.status(HttpStatus.OK) - .body(new ApiResponseBody<>(true, reservationService.getAllByEventId(eventId))); + @GetMapping("/{reservationId}") + public ResponseEntity<ApiResponseBody<ReservationResponse>> getReservationById( + @PathVariable Long reservationId) { + return ApiResponse.ok(reservationService.getById(reservationId)); } @PostMapping public ResponseEntity<ApiResponseBody<KakaoPayResponse>> createReservation( - @Valid @RequestBody ReservationRequest reservationRequest, @Authorize({MemberType.curator, MemberType.normal}) MemberIdentifier memberIdentifier) { - return ResponseEntity.status(HttpStatus.CREATED) - .body(new ApiResponseBody<>(true, reservationService.create(memberIdentifier.id(), reservationRequest))); + @Valid @RequestBody ReservationRequest reservationRequest, + @Authorize({MemberType.normal, MemberType.curator}) MemberIdentifier member) { + return ApiResponse.created(reservationService.create(member.id(), reservationRequest)); } @DeleteMapping("/{reservationId}") public ResponseEntity<ApiResponseBody<Void>> deleteReservation( - @PathVariable Long reservationId) { - reservationService.delete(reservationId); - return ResponseEntity.status(HttpStatus.NO_CONTENT) - .body(new ApiResponseBody<>(true, null)); + @PathVariable Long reservationId, + @Authorize({MemberType.normal, MemberType.curator}) MemberIdentifier member) { + reservationService.delete(member.id(), reservationId); + return ApiResponse.noContent(); } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationRequest.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationRequest.java similarity index 54% rename from src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationRequest.java rename to src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationRequest.java index fe69e90e..aa7c11e1 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationRequest.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationRequest.java @@ -1,5 +1,8 @@ -package org.ktc2.cokaen.wouldyouin.reservation.application.dto; +package org.ktc2.cokaen.wouldyouin.reservation.api.dto; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; import lombok.Builder; import lombok.Getter; import org.ktc2.cokaen.wouldyouin.event.persist.Event; @@ -10,8 +13,15 @@ @Builder(toBuilder = true) public class ReservationRequest { + @NotNull(message = "이벤트 ID는 필수입니다.") private Long eventId; + + @NotNull(message = "가격은 필수입니다.") + @Min(value = 0, message = "가격은 0 이상이어야 합니다.") + @Max(value = 1000000, message = "가격은 1,000,000원 이하이어야 합니다.") private Integer price; + + @NotNull(message = "수량은 필수입니다.") private Integer quantity; public Reservation toEntity(Member member, Event event) { diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationResponse.java similarity index 96% rename from src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationResponse.java rename to src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationResponse.java index 1cd8a346..ba63a297 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/dto/ReservationResponse.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationResponse.java @@ -1,4 +1,4 @@ -package org.ktc2.cokaen.wouldyouin.reservation.application.dto; +package org.ktc2.cokaen.wouldyouin.reservation.api.dto; import java.time.LocalDateTime; import lombok.Builder; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationSliceResponse.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationSliceResponse.java new file mode 100644 index 00000000..61c076dc --- /dev/null +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/api/dto/ReservationSliceResponse.java @@ -0,0 +1,24 @@ +package org.ktc2.cokaen.wouldyouin.reservation.api.dto; + +import java.util.List; +import lombok.Builder; +import lombok.Getter; +import org.ktc2.cokaen.wouldyouin._common.api.SliceInfo; + +@Getter +@Builder +public class ReservationSliceResponse { + + private List<ReservationResponse> reservations; + private SliceInfo sliceInfo; + + public static ReservationSliceResponse of(List<ReservationResponse> reservations, int size, Long lastId) { + return ReservationSliceResponse.builder() + .reservations(reservations) + .sliceInfo(SliceInfo.builder() + .sliceSize(size) + .lastId(lastId) + .build()) + .build(); + } +} diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/ReservationService.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/ReservationService.java index 70afa7fa..c94af33a 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/ReservationService.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/application/ReservationService.java @@ -3,16 +3,19 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.ktc2.cokaen.wouldyouin._common.exception.EntityNotFoundException; +import org.ktc2.cokaen.wouldyouin._common.exception.UnauthorizedException; import org.ktc2.cokaen.wouldyouin.event.application.EventService; import org.ktc2.cokaen.wouldyouin.member.application.MemberService; -import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayRequest; import org.ktc2.cokaen.wouldyouin.payment.application.PaymentService; import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayRequest; import org.ktc2.cokaen.wouldyouin.payment.dto.KakaoPayResponse; -import org.ktc2.cokaen.wouldyouin.reservation.application.dto.ReservationRequest; -import org.ktc2.cokaen.wouldyouin.reservation.application.dto.ReservationResponse; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationRequest; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationResponse; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationSliceResponse; import org.ktc2.cokaen.wouldyouin.reservation.persist.Reservation; import org.ktc2.cokaen.wouldyouin.reservation.persist.ReservationRepository; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -26,23 +29,24 @@ public class ReservationService { private final EventService eventService; @Transactional(readOnly = true) - public List<ReservationResponse> getAll() { - return reservationRepository.findAll().stream() - .map(ReservationResponse::from).toList(); + public ReservationSliceResponse getAllByMemberId(Long memberId, Pageable pageable, Long lastId) { + return getReservationSliceResponse( + reservationRepository.findByMemberIdOrderByReservationIdDesc(memberId, lastId, pageable), lastId); } @Transactional(readOnly = true) - public List<ReservationResponse> getAllByMemberId(Long memberId) { - return reservationRepository.findByMemberId(memberId).stream() - .map(ReservationResponse::from) - .toList(); + public ReservationSliceResponse getAllByEventId(Long eventId, Pageable pageable, Long lastId) { + return getReservationSliceResponse( + reservationRepository.findByEventIdOrderByReservationIdDesc(eventId, lastId, pageable), lastId); } - @Transactional(readOnly = true) - public List<ReservationResponse> getAllByEventId(Long eventId) { - return reservationRepository.findByEventId(eventId).stream() - .map(ReservationResponse::from) - .toList(); + private ReservationSliceResponse getReservationSliceResponse(Slice<Reservation> reservationSlice, Long lastId) { + List<ReservationResponse> reservations = reservationSlice.stream().map(ReservationResponse::from).toList(); + if (!reservationSlice.hasContent()) { + Long id = reservationSlice.getContent().getLast().getId(); + return ReservationSliceResponse.of(reservations, reservationSlice.getSize(), id); + } + return ReservationSliceResponse.of(reservations, reservationSlice.getSize(), lastId); } @Transactional(readOnly = true) @@ -61,8 +65,16 @@ public KakaoPayResponse create(Long memberId, ReservationRequest reservationRequ } @Transactional - public void delete(Long id) { - reservationRepository.findById(id).orElseThrow(() -> new EntityNotFoundException("reservation ")); - reservationRepository.deleteById(id); + public void delete(Long memberId, Long reservationId) { + Reservation reservation = reservationRepository.findById(reservationId) + .orElseThrow(() -> new EntityNotFoundException("reservation ")); + validateMemberId(memberId, reservation); + reservationRepository.deleteById(reservationId); + } + + public void validateMemberId(Long memberId, Reservation reservation) { + if (!memberId.equals(reservation.getMember().getId())) { + throw new UnauthorizedException("member ID가 예약의 member ID와 일치하지 않습니다."); + } } } \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/Reservation.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/Reservation.java index 36ef2d4d..fc6ae657 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/Reservation.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/Reservation.java @@ -3,6 +3,8 @@ import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @@ -19,10 +21,12 @@ import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.member.persist.Member; import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) +@EntityListeners(AuditingEntityListener.class) @Entity public class Reservation { @@ -31,11 +35,11 @@ public class Reservation { @Column(name = "reservation_id") private Long id; - @ManyToOne(cascade = CascadeType.ALL) + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "member_id") private Member member; - @OneToOne(cascade = CascadeType.ALL) + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "event_id") private Event event; diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/ReservationRepository.java b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/ReservationRepository.java index d2212546..bddac660 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/ReservationRepository.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/reservation/persist/ReservationRepository.java @@ -1,12 +1,21 @@ package org.ktc2.cokaen.wouldyouin.reservation.persist; -import java.util.List; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; @Repository public interface ReservationRepository extends JpaRepository<Reservation, Long> { - public List<Reservation> findByMemberId(Long memberId); - public List<Reservation> findByEventId(Long eventId); -} + @Query("SELECT R FROM Reservation R JOIN FETCH R.member JOIN FETCH R.event " + + "WHERE R.member.Id = :memberId AND R.id > :lastId " + + "ORDER BY R.id DESC") + Slice<Reservation> findByMemberIdOrderByReservationIdDesc(Long memberId, Long lastId, Pageable pageable); + + @Query("SELECT R FROM Reservation R JOIN FETCH R.member JOIN FETCH R.event " + + "WHERE R.event.id = :eventId AND R.id > :lastId " + + "ORDER BY R.id DESC") + Slice<Reservation> findByEventIdOrderByReservationIdDesc(Long eventId, Long lastId, Pageable pageable); +} \ No newline at end of file diff --git a/src/main/java/org/ktc2/cokaen/wouldyouin/review/api/ReviewController.java b/src/main/java/org/ktc2/cokaen/wouldyouin/review/api/ReviewController.java index a6be58a6..ea036dc1 100644 --- a/src/main/java/org/ktc2/cokaen/wouldyouin/review/api/ReviewController.java +++ b/src/main/java/org/ktc2/cokaen/wouldyouin/review/api/ReviewController.java @@ -25,6 +25,8 @@ public class ReviewController { private final ReviewService reviewService; + // ToDo: ApiResponse 정적메서드 활용 + @GetMapping("/{memberId}") public ResponseEntity<ApiResponseBody<List<ReviewResponse>>> getReviewsByMemberId( @PathVariable("memberId") Long memberId) { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 083d7121..e76ba79b 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,8 +3,11 @@ spring: application.name: WouldYouIn + + # 임시 프로젝트 url + wouldyouin-domain-name: http://52.78.71.136/ profiles: - default: ${ACTIVE_PROFILE} + default: dev # 소셜로그인 테스트시 캐시 비활성화 thymeleaf: @@ -28,6 +31,7 @@ spring: properties: hibernate: format_sql: true + default_batch_fetch_size: 100 show-sql: true hibernate.ddl-auto: create diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestData.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestData.java new file mode 100644 index 00000000..d1bdbf13 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestData.java @@ -0,0 +1,375 @@ +package org.ktc2.cokaen.wouldyouin._global; + +import java.time.LocalDateTime; +import java.util.List; +import org.ktc2.cokaen.wouldyouin.Image.persist.CurationImage; +import org.ktc2.cokaen.wouldyouin.Image.persist.EventImage; +import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; +import org.ktc2.cokaen.wouldyouin._common.api.SliceInfo; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCardRequest; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCardResponse; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCreateRequest; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationEditRequest; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationResponse; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationSliceResponse; +import org.ktc2.cokaen.wouldyouin.curation.persist.Curation; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; +import org.ktc2.cokaen.wouldyouin.event.api.dto.relationResonse.CurationEventResponse; +import org.ktc2.cokaen.wouldyouin.event.api.dto.relationResonse.ReservationEventResponse; +import org.ktc2.cokaen.wouldyouin.event.persist.Event; +import org.ktc2.cokaen.wouldyouin.member.application.dto.relation.ReservationMemberResponse; +import org.ktc2.cokaen.wouldyouin.member.application.dto.relationResponse.CurationCuratorResponse; +import org.ktc2.cokaen.wouldyouin.member.persist.AccountType; +import org.ktc2.cokaen.wouldyouin.member.persist.Curator; +import org.ktc2.cokaen.wouldyouin.member.persist.Host; +import org.ktc2.cokaen.wouldyouin.member.persist.Member; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationRequest; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationResponse; +import org.ktc2.cokaen.wouldyouin.reservation.api.dto.ReservationSliceResponse; +import org.ktc2.cokaen.wouldyouin.reservation.persist.Reservation; +import org.springframework.test.util.ReflectionTestUtils; + +public class TestData { + + public static SliceInfo createSliceInfo() { + return SliceInfo.builder() + .sliceSize(10) + .lastId(100L) + .build(); + } + + public static class ImageDomain { + + public static MemberImage createValidMemberImage(Long id) { + MemberImage ret = MemberImage.builder() + .url("memberImageUrl") + .size(10L) + .extension(".jpg") + .build(); + ReflectionTestUtils.setField(ret, "id", id); + return ret; + } + + public static EventImage createValidEventImage(Long id) { + EventImage ret = EventImage.builder() + .url("eventImageUrl") + .size(10L) + .extension(".jpg") + .build(); + ReflectionTestUtils.setField(ret, "id", id); + return ret; + } + + public static CurationImage createValidCurationImage(Long id) { + CurationImage ret = CurationImage.builder() + .url("curationImageUrl") + .size(10L) + .extension(".jpg") + .build(); + ReflectionTestUtils.setField(ret, "id", id); + return ret; + } + } + + public static class MemberDomain { + + public static final long validMemberId = 1L; + public static final long validCuratorId = 2L; + public static final long validHostId = 3L; + public static final long validWelcomeMemberId = 4L; + + public static Member createValidMember() { + MemberImage memberImage = ImageDomain.createValidMemberImage(validMemberId); + Member ret = Member.builder() + .accountType(AccountType.kakao) + .email("member1@example.com") + .nickname("nick_normal_123") + .phone("010-1112-2233") + .profileImage(memberImage) + .area(Area.광주) + .gender("Men") + .socialId("100100100100100") + .build(); + ReflectionTestUtils.setField(ret, "Id", validMemberId); + ReflectionTestUtils.setField(ret, "memberType", MemberType.normal); + ReflectionTestUtils.setField(memberImage, "baseMember", ret); + return ret; + } + + public static Curator createValidCurator() { + MemberImage memberImage = ImageDomain.createValidMemberImage(validCuratorId); + Curator ret = Curator.curatorBuilder() + .accountType(AccountType.google) + .email("curator1@example.com") + .nickname("nick_curator_12") + .phone("010-4545-6767") + .profileImage(memberImage) + .area(Area.광주) + .gender("Women") + .socialId("200200200200200") + .build(); + ReflectionTestUtils.setField(ret, "Id", validCuratorId); + ReflectionTestUtils.setField(ret, "intro", "큐레이터 자기소개입니다."); + ReflectionTestUtils.setField(ret, "hashtag", "#큐레이터#해시태그#입니다"); + ReflectionTestUtils.setField(memberImage, "baseMember", ret); + return ret; + } + + public static Host createValidHost() { + MemberImage memberImage = ImageDomain.createValidMemberImage(validHostId); + Host ret = Host.builder() + .email("curator1@example.com") + .nickname("nick_curator_12") + .phone("010-4545-6767") + .hashedPassword("hashed_password") + .profileImage(memberImage) + .build(); + ReflectionTestUtils.setField(ret, "Id", validHostId); + ReflectionTestUtils.setField(ret, "intro", "주최자 자기소개입니다."); + ReflectionTestUtils.setField(ret, "hashtag", "#주최자#해시태그#입니다"); + ReflectionTestUtils.setField(memberImage, "baseMember", ret); + return ret; + } + + public static Member createValidWelcomeMember() { + MemberImage memberImage = ImageDomain.createValidMemberImage(validWelcomeMemberId); + Member ret = Member.builder() + .accountType(AccountType.kakao) + .email("member2@example.com") + .nickname("nick_normal_333") + .phone("010-4414-1144") + .profileImage(memberImage) + .area(Area.서울) + .gender("Men") + .socialId("456456456456") + .build(); + ReflectionTestUtils.setField(ret, "Id", validWelcomeMemberId); + ReflectionTestUtils.setField(memberImage, "baseMember", ret); + return ret; + } + + public static ReservationMemberResponse createValidReservationMemberResponse() { + return ReservationMemberResponse.builder() + .id(validMemberId) + .email("member1@example.com") + .nickname("nick_normal_123") + .phone("010-1112-2233") + .gender("Men") + .build(); + } + } + + public static class EventDomain { + + public static Event createValidEvent() { + Event validEvent = Event.builder() + .title("title") + .content("content") + .area(Area.전체) + .location(new Location(132.0, 43.0)) + .startTime(LocalDateTime.now()) + .endTime(LocalDateTime.now()) + .price(10000) + .totalSeat(100) + .category(Category.밴드) + .build(); + validEvent.setHost(MemberDomain.createValidHost()); + return validEvent; + } + + public static EventCreateRequest createValidEventCreateRequest() { + return EventCreateRequest.builder() + .title("title") + .content("content 조홍식씨 최소글자 20자라고 해놓고 안 지켰어요.") + .area(Area.전체) + .location(new Location(132.0, 43.0)) + .startTime(LocalDateTime.of(2025, 10, 1, 9, 0)) + .endTime(LocalDateTime.of(2025, 10, 1, 10, 0)) + .price(10000) + .totalSeat(100) + .category(Category.밴드) + .imageIds(List.of()) + .build(); + } + + public static EventEditRequest createValidEventEditRequest() { + return EventEditRequest.builder() + .title("modifiedTitle") + .content("modifiedContent 조홍식씨 최소글자 20자라고 해놓고 안 지켰어요. ") + .area(Area.광주) + .location(new Location(232.0, 143.0)) + .startTime(LocalDateTime.of(2024, 10, 2, 17, 0)) + .endTime(LocalDateTime.of(2024, 10, 2, 18, 0)) + .price(20000) + .totalSeat(200) + .category(Category.뮤지컬) + .imageIds(List.of()) + .build(); + } + + public static CurationEventResponse createValidCurationEventResponse() { + return CurationEventResponse.builder() + .id(1L) + .title("title") + .location(new Location(132.0, 43.0)) + .thumbnailImageUrl("thumbnailImageUrl") + .hostProfileImageUrl("hostProfileImageUrl") + .hostNickname("nick_curator_12") + .build(); + } + + public static ReservationEventResponse createValidReservationEventResponse() { + return ReservationEventResponse.builder() + .id(1L) + .title("title") + .price(15000) + .build(); + } + } + + public static class ReservationDomain { + + public static ReservationRequest createValidReservationRequest() { + return ReservationRequest.builder() + .eventId(1L) + .price(15000) + .quantity(2) + .build(); + } + + public static Reservation createValidReservation() { + Reservation reservation = Reservation.builder() + .member(MemberDomain.createValidMember()) + .event(EventDomain.createValidEvent()) + .price(15000) + .quantity(2) + .build(); + ReflectionTestUtils.setField(reservation, "id", 1L); + return reservation; + } + + public static ReservationResponse createValidReservationResponse() { + return ReservationResponse.builder() + .id(1L) + .member(MemberDomain.createValidReservationMemberResponse()) + .event(EventDomain.createValidReservationEventResponse()) + .price(15000) + .quantity(2) + .reservationDate(LocalDateTime.of(2024, 3, 23, 0, 0)) + .build(); + } + + public static ReservationSliceResponse createValidReservationSliceResponse() { + return ReservationSliceResponse.builder() + .reservations(List.of(createValidReservationResponse())) + .sliceInfo(TestData.createSliceInfo()) + .build(); + } + } + + public static class CurationDomain { + + public static Curation createValidCuration() { + return Curation.builder() + .curator(MemberDomain.createValidCurator()) + .title("title") + .content("content") + .curationCards(List.of()) + .area(Area.전체) + .hashTag(List.of("#해시태그1", "#해시태그2")) + .events(List.of(EventDomain.createValidEvent())) + .build(); + } + + public static CurationCardRequest createValidCurationCardRequest1() { + return CurationCardRequest.builder() + .subtitle("부제목1") + .content("큐레이션 카드 내용1 입니다. 큐레이션 카드의 내용은 최소 20자 최대 1000자 입니다.") + .imageIds(List.of(1L, 2L)) + .build(); + } + + public static CurationCardRequest createValidCurationCardRequest2() { + return CurationCardRequest.builder() + .subtitle("부제목2") + .content("큐레이션 카드 내용2 입니다. 큐레이션 카드의 내용은 최소 20자 최대 1000자 입니다.") + .imageIds(List.of(3L, 4L)) + .build(); + } + + public static CurationCardResponse createCurationCardResponse1() { + return CurationCardResponse.builder() + .subtitle("부제목1") + .content("큐레이션 카드 내용1 입니다. 큐레이션 카드의 내용은 최소 20자 최대 1000자 입니다.") + .imageUrls(List.of("image1.com", "image2.com")) + .build(); + } + + public static CurationCardResponse createCurationCardResponse2() { + return CurationCardResponse.builder() + .subtitle("부제목2") + .content("큐레이션 카드 내용2 입니다. 큐레이션 카드의 내용은 최소 20자 최대 1000자 입니다.") + .imageUrls(List.of("image3.com", "image4.com")) + .build(); + } + + public static CurationCreateRequest createValidCurationCreateRequest() { + return CurationCreateRequest.builder() + .title("큐레이션 제목1") + .content("큐레이션 카드 내용1 입니다. 큐레이션 카드의 내용은 최소 20자 최대 1000자 입니다.") + .curationCards(List.of(createValidCurationCardRequest1())) + .area(Area.광주) + .hashTag(List.of("#광주밴드", "#전남대")) + .eventIds(List.of(1L, 2L)) + .build(); + } + + public static CurationEditRequest createValidCurationEditRequest() { + return CurationEditRequest.builder() + .title("큐레이션 제목2") + .content("큐레이션 내용2 입니다.") + .curationCards(List.of(createValidCurationCardRequest2())) + .area(Area.서울) + .hashTag(List.of("#서울밴드", "#서울대")) + .eventIds(List.of(3L, 4L)) + .build(); + } + + public static CurationCuratorResponse createCurationCuratorResponse() { + Curator curator = MemberDomain.createValidCurator(); + return CurationCuratorResponse.builder() + .nickname(curator.getNickname()) + .email(curator.getEmail()) + .phone(curator.getPhone()) + .profileImageUrl(curator.getProfileImageUrl()) + .intro(curator.getIntro()) + .likes(curator.getLikes()) + .hashtags(curator.getHashTagList()) + .build(); + } + + public static CurationResponse createValidCurationResponse() { + return CurationResponse.builder() + .curator(createCurationCuratorResponse()) + .title("title") + .content("content") + .curationCards(List.of(createCurationCardResponse1())) + .area(Area.전체) + .hashTag(List.of("#해시태그1", "#해시태그2")) + .eventsInfo(List.of(EventDomain.createValidCurationEventResponse())) + .build(); + } + + public static CurationSliceResponse createValidCurationSliceResponse() { + return CurationSliceResponse.builder() + .curations(List.of(createValidCurationResponse())) + .slice(TestData.createSliceInfo()) + .build(); + } + } +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestUtil.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestUtil.java new file mode 100644 index 00000000..50cbb345 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/TestUtil.java @@ -0,0 +1,13 @@ +package org.ktc2.cokaen.wouldyouin._global; + +public class TestUtil { + + public static <T> T getLeftOrRight(T left, T right) { + return Math.random() > 0.5 ? left : right; + } + + public static <T> T getOrNull(T object) { + return getLeftOrRight(object, null); + } + +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCurator.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCurator.java new file mode 100644 index 00000000..58488b23 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCurator.java @@ -0,0 +1,12 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; + +@Retention(RetentionPolicy.RUNTIME) +@WithMockCustomUser(memberId = MemberDomain.validCuratorId, memberType = MemberType.curator) +public @interface WithMockCurator { + +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUser.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUser.java new file mode 100644 index 00000000..742a7324 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUser.java @@ -0,0 +1,13 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.springframework.security.test.context.support.WithSecurityContext; + +@Retention(RetentionPolicy.RUNTIME) +@WithSecurityContext(factory = WithMockCustomUserSecurityContextFactory.class) +public @interface WithMockCustomUser { + long memberId(); + MemberType memberType(); +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUserSecurityContextFactory.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUserSecurityContextFactory.java new file mode 100644 index 00000000..4c164a2b --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockCustomUserSecurityContextFactory.java @@ -0,0 +1,22 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import org.ktc2.cokaen.wouldyouin.auth.MemberIdentifier; +import org.ktc2.cokaen.wouldyouin.auth.persist.CustomUserDetails; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.test.context.support.WithSecurityContextFactory; + +public class WithMockCustomUserSecurityContextFactory implements WithSecurityContextFactory<WithMockCustomUser> { + + @Override + public SecurityContext createSecurityContext(WithMockCustomUser customUserAnnotation) { + SecurityContext context = SecurityContextHolder.createEmptyContext(); + + CustomUserDetails principal = new CustomUserDetails(new MemberIdentifier(customUserAnnotation.memberId(), customUserAnnotation.memberType())); + Authentication auth = new UsernamePasswordAuthenticationToken(principal, principal.getPassword(), principal.getAuthorities()); + context.setAuthentication(auth); + return context; + } +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockHost.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockHost.java new file mode 100644 index 00000000..1805cbf8 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockHost.java @@ -0,0 +1,12 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; + +@Retention(RetentionPolicy.RUNTIME) +@WithMockCustomUser(memberId = MemberDomain.validHostId, memberType = MemberType.host) +public @interface WithMockHost { + +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockMember.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockMember.java new file mode 100644 index 00000000..0bbaf38c --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockMember.java @@ -0,0 +1,12 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; + +@Retention(RetentionPolicy.RUNTIME) +@WithMockCustomUser(memberId = MemberDomain.validMemberId, memberType = MemberType.normal) +public @interface WithMockMember { + +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockWelcomeMember.java b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockWelcomeMember.java new file mode 100644 index 00000000..1f8e877b --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/_global/mockMember/WithMockWelcomeMember.java @@ -0,0 +1,12 @@ +package org.ktc2.cokaen.wouldyouin._global.mockMember; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; + +@Retention(RetentionPolicy.RUNTIME) +@WithMockCustomUser(memberId = MemberDomain.validWelcomeMemberId, memberType = MemberType.welcome) +public @interface WithMockWelcomeMember { + +} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/curation/CurationControllerUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/curation/CurationControllerUnitTest.java new file mode 100644 index 00000000..d6fcb308 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/curation/CurationControllerUnitTest.java @@ -0,0 +1,628 @@ +package org.ktc2.cokaen.wouldyouin.curation; + +import static java.lang.Math.abs; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; +import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; +import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.util.List; +import java.util.Random; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._global.TestData.CurationDomain; +import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockCurator; +import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockHost; +import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockMember; +import org.ktc2.cokaen.wouldyouin.auth.application.JwtAuthFilter; +import org.ktc2.cokaen.wouldyouin.curation.api.CurationController; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCardRequest; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationCreateRequest; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.CurationEditRequest; +import org.ktc2.cokaen.wouldyouin.curation.application.CurationService; +import org.mockito.ArgumentCaptor; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.http.MediaType; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; +import org.springframework.web.context.WebApplicationContext; + +@WebMvcTest(CurationController.class) +class CurationControllerUnitTest { + + private static ObjectMapper objectMapper; + + @MockBean + private CurationService curationService; + + @MockBean + private JwtAuthFilter jwtAuthFilter; + + @Autowired + private MockMvc mockMvc; + + @Autowired + private WebApplicationContext context; + private static final long randomId = abs(new Random().nextLong()); + + + @BeforeAll + public static void init() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } + + @BeforeEach + public void setup() throws Exception { + mockMvc = MockMvcBuilders + .webAppContextSetup(context) + .apply(springSecurity()) + .build(); + } + + @Test + @DisplayName("RequestParam을 통해 전달받은 지역의 큐레이션 목록을 조회한다.") + @WithMockMember + void getCurationsByAreaOrderByCreatedDateDesc1() throws Exception { + // given, when + mockMvc.perform(get("/api/curations") + .param("area", Area.광주.name()) + .param("page", "5") + .param("size", "20") + .param("lastId", "100")).andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).getAllByAreaOrderByCreatedDateDesc( + eq(Area.광주), eq(PageRequest.of(5, 20)), eq(100L)); + } + + @Test + @DisplayName("RequestParam을 통해 지역을 지정하지 않은 경우, 전체 지역의 큐레이션 목록을 조회한다.") + @WithMockMember + void getCurationsByAreaOrderByCreatedDateDesc2() throws Exception { + // given, when + mockMvc.perform(get("/api/curations")).andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).getAllByAreaOrderByCreatedDateDesc( + eq(Area.전체), eq(PageRequest.of(0, 10)), eq(Long.MAX_VALUE)); + } + + @Test + @DisplayName("ReqeustParam을 통해 요청할 페이지에 대한 정보를 전달받아, 해당하는 호스트의 큐레이션 목록을 조회한다.") + @WithMockMember + void getCurationsByCuratorIdOrderByCreatedDateDesc1() throws Exception { + // given, when + mockMvc.perform(get("/api/curations/curators/" + randomId) + .param("page", "5") + .param("size", "20") + .param("lastId", "100")).andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).getAllByCuratorIdOrderByCreatedDateDesc( + eq(randomId), eq(PageRequest.of(5, 20)), eq(100L)); + } + + @Test + @DisplayName("RequestParam을 통해 페이지 정보를 지정하지 않은 경우, 디폴트 값으로 해당하는 호스트의 큐레이션 목록을 조회한다.") + @WithMockMember + void getCurationsByCuratorIdOrderByCreatedDateDesc2() throws Exception { + // given, when + mockMvc.perform(get("/api/curations/curators/" + randomId)).andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).getAllByCuratorIdOrderByCreatedDateDesc( + eq(randomId), eq(PageRequest.of(0, 10)), eq(Long.MAX_VALUE)); + } + + @Test + @DisplayName("큐레이션 ID를 통해 해당하는 큐레이션을 조회한다.") + @WithMockMember + void getCurationByCurationId() throws Exception { + // given, when + mockMvc.perform(get("/api/curations/" + randomId)).andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).getById(eq(randomId)); + } + + @Test + @DisplayName("RequestBody로 전달받은 정보를 통해 큐레이션을 생성한다.") + @WithMockCurator + void createCuration1() throws Exception { + // given + ArgumentCaptor<CurationCreateRequest> captor = ArgumentCaptor.forClass(CurationCreateRequest.class); + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isCreated()); + + // then + then(curationService).should(times(1)).create(eq(MemberDomain.validCuratorId), captor.capture()); + assertThat(captor.getValue()).isEqualTo(request); + } + + @Test + @DisplayName("호스트의 권한으로는 큐레이션을 생성할 수 없다.") + @WithMockHost + void createCuration2() throws Exception { + // given, when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(CurationDomain.createValidCurationCreateRequest()))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("멤버의 권한으로는 큐레이션을 생성할 수 없다.") + @WithMockMember + void createCuration3() throws Exception { + // given, when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(CurationDomain.createValidCurationCreateRequest()))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 제목에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void createCuration4() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .title("").build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("제목은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 큐레이션 카드의 부제목에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void createCuration5() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder().subtitle("").build())) + .build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("부제목은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 큐레이션 카드의 내용에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void createCuration6() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder().content(null).build())) + .build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("내용은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 큐레이션 카드의 내용에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void createCuration7() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder().content(null).build())) + .build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("내용은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 큐레이션 카드의 내용은 20자 이상 1000자 이하이어야 한다.") + @WithMockCurator + void createCuration8() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder().content("짧은 내용").build())) + .build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("내용은 20자 이상 1000자 이하입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 각 큐레이션 카드에는 이미지를 최대 5개까지 등록할 수 있다.") + @WithMockCurator + void createCuration9() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder() + .imageIds(List.of(1L, 2L, 3L, 4L, 5L, 6L)).build())) + .build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("이미지는 최대 5개까지 등록할 수 있습니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 지역에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void createCuration10() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder() + .area(null).build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("지역은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 생성 시, 큐레이션 카드의 개수는 1개 이상 10개 이하이어야 한다.") + @WithMockCurator + void createCuration11() throws Exception { + // given + CurationCreateRequest request = CurationDomain.createValidCurationCreateRequest().toBuilder(). + curationCards(List.of()).build(); + + // when + mockMvc.perform(post("/api/curations") + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("큐레이션 카드의 개수는 1개 이상 10개 이하이어야 합니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("RequestBody로 전달받은 정보를 통해 큐레이션을 수정한다.") + @WithMockCurator + void updateCuration1() throws Exception { + // given + ArgumentCaptor<CurationEditRequest> captor = ArgumentCaptor.forClass(CurationEditRequest.class); + CurationEditRequest request = CurationDomain.createValidCurationEditRequest(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isOk()); + + // then + then(curationService).should(times(1)).update(eq(MemberDomain.validCuratorId), eq(randomId), captor.capture()); + assertThat(captor.getValue()).isEqualTo(request); + } + + @Test + @DisplayName("Host의 권한으로는 큐레이션을 수정할 수 없다.") + @WithMockHost + void updateCuration2() throws Exception { + // given, when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(CurationDomain.createValidCurationEditRequest()))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("Member의 권한으로는 큐레이션을 수정할 수 없다.") + @WithMockMember + void updateCuration3() throws Exception { + // given, when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(CurationDomain.createValidCurationEditRequest()))) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 제목에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void updateCuration4() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .title(null).build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("제목은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 큐레이션 카드의 부제목에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void updateCuration5() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder() + .subtitle("").build())) + .build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("부제목은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 큐레이션 카드의 내용에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void updateCuration6() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder() + .content(null).build())) + .build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("내용은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 큐레이션 카드의 내용의 길이는 20자 이상 1000자 이하이어야 한다.") + @WithMockCurator + void updateCuration7() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder() + .content("짧은 내용").build())) + .build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("내용은 20자 이상 1000자 이하입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 각 큐레이션 카드에는 이미지를 최대 5개 등록할 수 있다.") + @WithMockCurator + void updateCuration8() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .curationCards(List.of(CurationDomain.createValidCurationCardRequest1().toBuilder() + .imageIds(List.of(1L, 2L, 3L, 4L, 5L, 6L)).build())) + .build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("이미지는 최대 5개까지 등록할 수 있습니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 지역에는 빈 값이 들어갈 수 없다.") + @WithMockCurator + void updateCuration9() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .area(null).build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("지역은 필수입니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("큐레이션 수정 시, 큐레이션 카드는 1개 이상 10개 이하이어야 한다.") + @WithMockCurator + void updateCuration10() throws Exception { + // given + CurationEditRequest request = CurationDomain.createValidCurationEditRequest().toBuilder() + .curationCards(List.of()).build(); + + // when + mockMvc.perform(put("/api/curations/" + randomId) + .with(csrf()) + .contentType(MediaType.APPLICATION_JSON) + .content(objectMapper.writeValueAsString(request))) + .andDo(print()) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("큐레이션 카드의 개수는 1개 이상 10개 이하이어야 합니다.")); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("PathVariable로 전달받은 큐레이션 ID에 해당하는 큐레이션을 삭제한다.") + @WithMockCurator + void deleteCuration1() throws Exception { + // given, when + mockMvc.perform(delete("/api/curations/" + randomId) + .with(csrf())) + .andDo(print()) + .andExpect(status().isNoContent()); + + // then + then(curationService).should(times(1)).delete(eq(MemberDomain.validCuratorId), eq(randomId)); + } + + @Test + @DisplayName("Host의 권한으로는 큐레이션을 삭제할 수 없다.") + @WithMockHost + void deleteCuration2() throws Exception { + // given, when + mockMvc.perform(delete("/api/curations/" + randomId) + .with(csrf())) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } + + @Test + @DisplayName("Member의 권한으로는 큐레이션을 삭제할 수 없다.") + @WithMockMember + void deleteCuration3() throws Exception { + // given, when + mockMvc.perform(delete("/api/curations/" + randomId) + .with(csrf())) + .andDo(print()) + .andExpect(status().isUnauthorized()); + + // then + then(curationService).shouldHaveNoInteractions(); + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventControllerUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventControllerUnitTest.java index 900fee8c..e9267358 100644 --- a/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventControllerUnitTest.java +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventControllerUnitTest.java @@ -1,67 +1,80 @@ package org.ktc2.cokaen.wouldyouin.event; -import static java.lang.Math.abs; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidHost; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.verify; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.util.Random; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; + +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; import org.ktc2.cokaen.wouldyouin.auth.application.JwtAuthFilter; import org.ktc2.cokaen.wouldyouin.auth.application.JwtService; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.LocationFilter; import org.ktc2.cokaen.wouldyouin.event.api.EventController; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventSliceResponse; import org.ktc2.cokaen.wouldyouin.event.application.EventService; -import org.ktc2.cokaen.wouldyouin.global.TestData; +import org.ktc2.cokaen.wouldyouin._global.TestData.EventDomain; +import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockHost; import org.ktc2.cokaen.wouldyouin.member.application.MemberService; +import org.ktc2.cokaen.wouldyouin.member.persist.Host; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.web.context.WebApplicationContext; -@WithMockUser(username = "user", roles = {"USER"}) @WebMvcTest(EventController.class) class EventControllerUnitTest { + private static Long id; + private static Host validHost; + private static ObjectMapper objectMapper; @MockBean private EventService eventService; - @MockBean private MemberService memberService; - @MockBean private JwtService jwtService; - @MockBean private JwtAuthFilter jwtAuthFilter; - + private EventSliceResponse eventSliceResponse; @Autowired private MockMvc mockMvc; - @Autowired private WebApplicationContext context; - private static Long id; - - private static ObjectMapper objectMapper; + @BeforeAll + public static void init() { + id = 3L; + validHost = createValidHost(); + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } @BeforeEach public void setup() throws Exception { @@ -71,62 +84,140 @@ public void setup() throws Exception { .build(); } - @BeforeAll - public static void init() { - id = abs(new Random().nextLong()); - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - } - @Test @DisplayName("모든 행사 조회 - 성공") - void getEvents() throws Exception { - mockMvc.perform(get("/api/events")).andExpect(status().isOk()); - verify(eventService).getAll(); + @WithMockHost + void getEventsByFilterOrderByDistanceAsc() throws Exception { + // given + LocationFilter locationFilter = new LocationFilter(0.0, 0.0, 10.0, 10.0); + Location currentLocation = new Location(3.0, 2.0); + Category category = Category.공예; + Area area = Area.광주; + int pageNumber = 1; // 교수님 + int pageSize = 10; // + Long lastId = 1L; + Pageable pageable = PageRequest.of(pageNumber, pageSize); + + given(eventService.getAllByFilterOrderByDistanceAsc( + locationFilter, + currentLocation, + category, + area, + pageable, + lastId)) + .willReturn(eventSliceResponse); + + // when + mockMvc.perform(get("/api/events") + .param("startLatitude", locationFilter.getStartLatitude().toString()) + .param("startLongitude", locationFilter.getStartLongitude().toString()) + .param("endLatitude", locationFilter.getEndLatitude().toString()) + .param("endLongitude", locationFilter.getEndLongitude().toString()) + .param("latitude", currentLocation.getLatitude().toString()) + .param("longitude", currentLocation.getLongitude().toString()) + .param("category", category.toString()) // enum을 문자열로 변환하여 설정 + .param("area", area.toString()) // enum을 문자열로 변환하여 설정 + .param("page", String.valueOf(pageNumber)) + .param("size", String.valueOf(pageSize)) + .param("lastId", String.valueOf(lastId)) + ).andDo(print()) + .andExpect(status().isOk()); + + // then + then(eventService).should(times(1)).getAllByFilterOrderByDistanceAsc( + any(LocationFilter.class), + any(Location.class), + eq(category), + eq(area), + eq(pageable), + eq(lastId) + ); } @Test @DisplayName("주최자 id를 통한 모든 행사 조회 - 성공") - void getEventsByHostId() throws Exception { - mockMvc.perform(get("/api/events/hosts/" + id)).andExpect(status().isOk()); - verify(eventService).getAllByHostId(id); + @WithMockHost + void getEventsByLocationByHostId() throws Exception { + // given + int pageNumber = 1; + int pageSize = 10; + Pageable pageable = PageRequest.of(pageNumber, pageSize); + Long lastId = 1L; + + // when + mockMvc.perform(get("/api/events/hosts/" + id) + .param("page", String.valueOf(pageNumber)) + .param("size", String.valueOf(pageSize)) + .param("lastId", String.valueOf(lastId)) + ) + .andExpect(status().isOk()); + + //then + then(eventService).should(times(1)).getAllByHostIdOrderByCreatedDateDesc( + eq(id), + eq(pageable), + eq(lastId) + ); } @Test @DisplayName("행사 id를 통한 행사 조회 - 성공") + @WithMockHost void getEventByEventId() throws Exception { - mockMvc.perform(get("/api/events/" + id)).andExpect(status().isOk()); - verify(eventService).getById(id); + //given + //when + mockMvc.perform(get("/api/events/" + id)) + .andDo(print()) + .andExpect(status().isOk()); + + //then + then(eventService).should(times(1)).getById(eq(id)); } @Test @DisplayName("행사 생성 - 성공") + @WithMockHost void createEvent() throws Exception { + //given + //when mockMvc.perform(post("/api/events") .with(csrf()) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(TestData.validEventCreateRequest))) + .content(objectMapper.writeValueAsString(EventDomain.createValidEventCreateRequest()))) + .andDo(print()) .andExpect(status().isCreated()); - verify(eventService).create(any(EventCreateRequest.class)); + + //then + then(eventService).should(times(1)).create(eq(id), any(EventCreateRequest.class)); } @Test @DisplayName("행사 수정 - 성공") + @WithMockHost void updateEvent() throws Exception { + //given + //when mockMvc.perform(put("/api/events/" + id) .with(csrf()) .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(TestData.validEventCreateRequest))) + .content(objectMapper.writeValueAsString(EventDomain.createValidEventCreateRequest()))) .andExpect(status().isOk()); - verify(eventService).update(eq(id), any(EventEditRequest.class)); + + //then + then(eventService).should(times(1)).update(eq(id), eq(id), any(EventEditRequest.class)); } @Test @DisplayName("행사 삭제 - 성공") + @WithMockHost void deleteEvent() throws Exception { + //given + //when mockMvc.perform(delete("/api/events/" + id) .with(csrf()) ).andExpect(status().isNoContent()); - verify(eventService).delete(id); + + //then + then(eventService).should(times(1)).delete(eq(id), eq(id)); } } \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventServiceUnitTest.java index 8aa48ba9..a5e5eaa7 100644 --- a/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventServiceUnitTest.java +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/event/EventServiceUnitTest.java @@ -1,29 +1,38 @@ package org.ktc2.cokaen.wouldyouin.event; -import static java.lang.Math.abs; import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; import java.util.List; import java.util.Optional; -import java.util.Random; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.ktc2.cokaen.wouldyouin.Image.application.EventImageService; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._common.vo.Category; +import org.ktc2.cokaen.wouldyouin._common.vo.Location; +import org.ktc2.cokaen.wouldyouin.curation.api.dto.LocationFilter; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; +import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; import org.ktc2.cokaen.wouldyouin.event.application.EventService; +import org.ktc2.cokaen.wouldyouin.event.persist.Event; import org.ktc2.cokaen.wouldyouin.event.persist.EventRepository; -import org.ktc2.cokaen.wouldyouin.global.TestData; +import org.ktc2.cokaen.wouldyouin._global.TestData.EventDomain; import org.ktc2.cokaen.wouldyouin.member.application.HostService; +import org.ktc2.cokaen.wouldyouin.member.application.MemberService; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.SliceImpl; @ExtendWith(MockitoExtension.class) class EventServiceUnitTest { @@ -39,94 +48,170 @@ class EventServiceUnitTest { @Mock private EventImageService eventImageService; - - private long id; + private Event validEvent; @BeforeEach void setUp() { eventService = new EventService(eventRepository, hostService, eventImageService); - id = abs(new Random().nextLong()); + validEvent = EventDomain.createValidEvent(); } @Test @DisplayName("모든 행사 조회 - 성공") - void getAll() { - when(eventRepository.findAll()).thenReturn(List.of()); - eventService.getAll(); - verify(eventRepository, times(1)).findAll(); + void getAllByFilterOrderByDistanceAsc() { + // given + LocationFilter location = new LocationFilter(); + Location currentLocation = new Location(3.0, 2.0); + Category category = Category.공예; + Area area = Area.광주; + int pageNumber = 1; + int pageSize = 10; + Pageable pageable = PageRequest.of(pageNumber, pageSize); + Long lastId = 1L; + given(eventRepository.findAllByFilterOrderByDistance(location.getStartLatitude(), + location.getStartLongitude(), + location.getEndLatitude(), location.getEndLongitude(), currentLocation.getLatitude(), + currentLocation.getLongitude(), + category, area, pageable)).willReturn(new SliceImpl<>(List.of())); + + // when + eventService.getAllByFilterOrderByDistanceAsc(location, currentLocation, category, area, + pageable, lastId); + + // then + then(eventRepository).should(times(1)) + .findAllByFilterOrderByDistance(any(Double.class), any(Double.class), any(Double.class), + any(Double.class), any(Double.class), any(Double.class), + any(Category.class), any(Area.class), any(Pageable.class)); } @Test @DisplayName("주최자 id를 통한 모든 행사 조회 - 성공") - void getAllByHostId() { - when(eventRepository.findByHostId(id)).thenReturn(List.of()); - eventService.getAllByHostId(id); - verify(eventRepository, times(1)).findByHostId(id); + void getAllByHostIdOrderByCreatedDateDesc() { + // given + Long hostId = validEvent.getHost().getId(); + int pageNumber = 1; + int pageSize = 10; + Pageable pageable = PageRequest.of(pageNumber, pageSize); + Long lastId = 1L; + given(eventRepository.findAllByHostIdOrderByEventIdDesc(hostId, lastId, pageable)) + .willReturn(new SliceImpl<>(List.of())); + + // when + eventService.getAllByHostIdOrderByCreatedDateDesc(hostId, pageable, lastId); + + // then + then(eventRepository).should(times(1)). + findAllByHostIdOrderByEventIdDesc(any(Long.class), any(Long.class), + any(Pageable.class)); } @Test @DisplayName("행사 id를 통한 행사 조회 - 성공") void getById() { - when(eventRepository.findById(id)).thenReturn(Optional.of(TestData.validEvent)); - eventService.getById(id); - verify(eventRepository, times(1)).findById(id); + // given + given(eventRepository.findById(validEvent.getId())).willReturn(Optional.of(validEvent)); + + // when + eventService.getById(validEvent.getId()); + + // then + then(eventRepository).should(times(1)).findById(validEvent.getId()); } @Test @DisplayName("유효하지 않은 행사 id를 통한 행사 조회 - 실패") void getByInvalidId() { - when(eventRepository.findById(id)).thenThrow(RuntimeException.class); - assertThrows(RuntimeException.class, () -> eventService.getById(id)); + // given + Long eventId = validEvent.getId(); + given(eventRepository.findById(eventId)).willThrow(RuntimeException.class); + + // when & then + assertThrows(RuntimeException.class, () -> eventService.getById(eventId)); } @Test @DisplayName("행사 생성 - 성공") void create() { - when(eventRepository.save(any())).thenReturn(TestData.validEvent); - eventService.create(TestData.validEventCreateRequest); - verify(eventRepository, times(1)).save(any()); + // given + Long hostId = validEvent.getHost().getId(); + EventCreateRequest validEventCreateRequest = EventDomain.createValidEventCreateRequest(); + given(eventRepository.save(any())).willReturn(validEvent); + + // when + eventService.create(hostId, validEventCreateRequest); + + // then + then(eventRepository).should(times(1)).save(any(Event.class)); } @Test @DisplayName("행사 id를 통한 행사 수정 - 성공") void update() { - var validEvent = TestData.validEvent; - System.out.println(validEvent.getId()); - when(eventRepository.findById(id)).thenReturn(Optional.of(validEvent)); - eventService.update(id, TestData.validEventEditRequest); - verify(eventRepository, times(1)).findById(id); + // given + Long eventId = validEvent.getId(); + Long hostId = validEvent.getHost().getId(); + EventEditRequest validEventEditRequest = EventDomain.createValidEventEditRequest(); + given(eventRepository.findById(eventId)).willReturn(Optional.of(validEvent)); + + // when + eventService.update(hostId, eventId, validEventEditRequest); + + // then + then(eventRepository).should(times(1)).findById(eventId); assertAll( - () -> assertEquals(validEvent.getTitle(), TestData.validEventEditRequest.getTitle()), - () -> assertEquals(validEvent.getContent(), TestData.validEventEditRequest.getContent()), - () -> assertEquals(validEvent.getArea(), TestData.validEventEditRequest.getArea()), - () -> assertEquals(validEvent.getLocation(), TestData.validEventEditRequest.getLocation()), - () -> assertEquals(validEvent.getStartTime(), TestData.validEventEditRequest.getStartTime()), - () -> assertEquals(validEvent.getEndTime(), TestData.validEventEditRequest.getEndTime()), - () -> assertEquals(validEvent.getPrice(), TestData.validEventEditRequest.getPrice()), - () -> assertEquals(validEvent.getTotalSeat(), TestData.validEventEditRequest.getTotalSeat()), - () -> assertEquals(validEvent.getCategory(), TestData.validEventEditRequest.getCategory()) + () -> assertEquals(validEvent.getTitle(), validEventEditRequest.getTitle()), + () -> assertEquals(validEvent.getContent(), validEventEditRequest.getContent()), + () -> assertEquals(validEvent.getArea(), validEventEditRequest.getArea()), + () -> assertEquals(validEvent.getLocation(), validEventEditRequest.getLocation()), + () -> assertEquals(validEvent.getStartTime(), validEventEditRequest.getStartTime()), + () -> assertEquals(validEvent.getEndTime(), validEventEditRequest.getEndTime()), + () -> assertEquals(validEvent.getPrice(), validEventEditRequest.getPrice()), + () -> assertEquals(validEvent.getTotalSeat(), validEventEditRequest.getTotalSeat()), + () -> assertEquals(validEvent.getCategory(), validEventEditRequest.getCategory()) ); } @Test @DisplayName("유효하지 않은 행사 id를 통한 행사 수정 - 실패") void updateByInvalidId() { - when(eventRepository.findById(id)).thenThrow(RuntimeException.class); - assertThrows(RuntimeException.class, () -> eventService.update(id, TestData.validEventEditRequest)); + // given + EventEditRequest request = EventDomain.createValidEventEditRequest(); + Long invalidHostId = 999L; + given(eventRepository.findById(invalidHostId)).willThrow(RuntimeException.class); + + // when & then + assertThrows(RuntimeException.class, + () -> eventService.update(invalidHostId, validEvent.getId(), + EventDomain.createValidEventEditRequest())); } @Test @DisplayName("행사 삭제 - 성공") void delete() { - when(eventRepository.findById(id)).thenReturn(Optional.of(TestData.validEvent)); - eventService.delete(id); - verify(eventRepository, times(1)).findById(id); + // given + Long eventId = validEvent.getId(); + Long hostId = validEvent.getHost().getId(); + + // when + given(eventRepository.findById(eventId)).willReturn(Optional.of(validEvent)); + willDoNothing().given(eventRepository).deleteById(eventId); + eventService.delete(hostId, eventId); + + // then + then(eventRepository).should(times(1)).findById(eventId); + then(eventRepository).should(times(1)).deleteById(eventId); } @Test @DisplayName("유효하지 않은 행사 id를 통한 행사 삭제 - 실패") void deleteByInvalidId() { - when(eventRepository.findById(id)).thenThrow(RuntimeException.class); - assertThrows(RuntimeException.class, () -> eventService.delete(id)); + // given + Long eventId = validEvent.getId(); + Long hostId = validEvent.getHost().getId(); + given(eventRepository.findById(eventId)).willThrow(RuntimeException.class); + + // when & then + assertThrows(RuntimeException.class, () -> eventService.delete(hostId, eventId)); } } \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/global/TestData.java b/src/test/java/org/ktc2/cokaen/wouldyouin/global/TestData.java deleted file mode 100644 index 8312e8ac..00000000 --- a/src/test/java/org/ktc2/cokaen/wouldyouin/global/TestData.java +++ /dev/null @@ -1,97 +0,0 @@ -package org.ktc2.cokaen.wouldyouin.global; - -import jakarta.persistence.Column; -import jakarta.persistence.EnumType; -import jakarta.persistence.Enumerated; -import jakarta.persistence.GeneratedValue; -import jakarta.persistence.GenerationType; -import jakarta.persistence.Id; -import jakarta.persistence.OneToMany; -import java.time.LocalDateTime; -import java.util.List; -import java.util.UUID; -import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; -import org.ktc2.cokaen.wouldyouin._common.persist.Area; -import org.ktc2.cokaen.wouldyouin._common.persist.Category; -import org.ktc2.cokaen.wouldyouin._common.persist.Location; -import org.ktc2.cokaen.wouldyouin.event.api.dto.EventEditRequest; -import org.ktc2.cokaen.wouldyouin.event.api.dto.EventCreateRequest; -import org.ktc2.cokaen.wouldyouin.event.persist.Event; -import org.ktc2.cokaen.wouldyouin.member.persist.AccountType; -import org.ktc2.cokaen.wouldyouin.member.persist.Host; -import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; -import org.ktc2.cokaen.wouldyouin.reservation.application.dto.ReservationRequest; -import org.ktc2.cokaen.wouldyouin.reservation.persist.Reservation; - -public class TestData { - - public static EventCreateRequest validEventCreateRequest; - public static EventEditRequest validEventEditRequest; - public static Event validEvent; - public static Host validHost; - public static ReservationRequest validReservationRequest; - public static Reservation validReservation; - - static { - validEventCreateRequest = EventCreateRequest.builder() - .hostId(1L) - .title("title") - .content("content") - .area(Area.전체) - .location(new Location(132.0, 43.0)) - .startTime(LocalDateTime.of(2024, 10, 1, 9, 0)) - .endTime(LocalDateTime.of(2024, 10, 1, 10, 0)) - .price(10000) - .totalSeat(100) - .category(Category.밴드) - .imageIds(List.of()) - .build(); - - validEventEditRequest = EventEditRequest.builder() - .title("modifiedTitle") - .content("modifiedContent") - .area(Area.광주) - .location(new Location(232.0, 143.0)) - .startTime(LocalDateTime.of(2024, 10, 2, 17, 0)) - .endTime(LocalDateTime.of(2024, 10, 2, 18, 0)) - .price(20000) - .totalSeat(200) - .category(Category.뮤지컬) - .imageIds(List.of()) - .build(); - - validEvent = Event.builder() - .title("title") - .content("content") - .area(Area.전체) - .location(new Location(132.0, 43.0)) - .startTime(LocalDateTime.now()) - .endTime(LocalDateTime.now()) - .price(10000) - .totalSeat(100) - .category(Category.밴드) - .build(); - - validHost = Host.builder() - .nickname("nickname") - .phone("010-1234-5678") - .hashedPassword(UUID.randomUUID().toString()) - .build(); - validEvent.setHost(validHost); - - validReservationRequest = - ReservationRequest.builder() - .eventId(1L) - .price(10000) - .quantity(1) - .build(); - - validReservation = - Reservation.builder() - .member(null) - .event(null) - .price(10000) - .quantity(3) - .build(); - } -} diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/member/api/MemberControllerUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/member/api/MemberControllerUnitTest.java new file mode 100644 index 00000000..f47537a6 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/member/api/MemberControllerUnitTest.java @@ -0,0 +1,48 @@ +package org.ktc2.cokaen.wouldyouin.member.api; + +import static org.junit.jupiter.api.Assertions.*; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.ktc2.cokaen.wouldyouin.auth.application.CustomUserDetailsService; +import org.ktc2.cokaen.wouldyouin.auth.application.JwtService; +import org.ktc2.cokaen.wouldyouin.member.application.BaseMemberService; +import org.ktc2.cokaen.wouldyouin.member.application.HostService; +import org.ktc2.cokaen.wouldyouin.member.application.MemberService; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.boot.test.mock.mockito.MockBean; + +@WebMvcTest(MemberController.class) +class MemberControllerUnitTest { + + @MockBean + private CustomUserDetailsService customUserDetailsService; + + @MockBean + private JwtService jwtService; + + @MockBean + private BaseMemberService baseMemberService; + + @MockBean + private MemberService memberService; + + @MockBean + private HostService hostService; + + @BeforeEach + void setUp() { + } + + @Test + void updateMember() { + } + + @Test + void findMember() { + } + + @Test + void deleteMember() { + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/BaseMemberServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/BaseMemberServiceUnitTest.java new file mode 100644 index 00000000..8a3d0abc --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/BaseMemberServiceUnitTest.java @@ -0,0 +1,191 @@ +package org.ktc2.cokaen.wouldyouin.member.application; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidCurator; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidHost; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidMember; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidWelcomeMember; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.validCuratorId; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.validHostId; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.validMemberId; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.validWelcomeMemberId; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.BDDMockito.willDoNothing; +import static org.mockito.Mockito.times; + +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.ktc2.cokaen.wouldyouin.member.application.dto.MemberResponse; +import org.ktc2.cokaen.wouldyouin.member.persist.BaseMember; +import org.ktc2.cokaen.wouldyouin.member.persist.BaseMemberRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.Curator; +import org.ktc2.cokaen.wouldyouin.member.persist.Host; +import org.ktc2.cokaen.wouldyouin.member.persist.Member; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class BaseMemberServiceUnitTest { + + @Mock + private BaseMemberRepository baseMemberRepository; + @Mock + private DerivedMemberServiceFactory derivedMemberServiceFactory; + + @Mock + private MemberServiceCommonBehavior mockMemberService; + @Mock + private MemberServiceCommonBehavior mockHostService; + @Mock + private MemberServiceCommonBehavior mockCuratorService; + + @Mock + private MemberResponse memberResponse; + + @InjectMocks + private BaseMemberService baseMemberService; + + private static Map<Long, BaseMember> members; + private static final Member validMember = createValidMember(); + private static final Member validWelcomeMember = createValidWelcomeMember(); + private static final Host validHost = createValidHost(); + private static final Curator validCurator = createValidCurator(); + + @BeforeAll + static void beforeAll() { + members = Map.of( + validMember.getId(), validMember, + validHost.getId(), validHost, + validCurator.getId(), validCurator, + validWelcomeMember.getId(), validWelcomeMember); + } + + @BeforeEach + void setUp() { + given(baseMemberRepository.findById(validMember.getId())).willReturn(Optional.of(validMember)); + given(baseMemberRepository.findById(validWelcomeMember.getId())).willReturn(Optional.of(validWelcomeMember)); + given(baseMemberRepository.findById(validHost.getId())).willReturn(Optional.of(validHost)); + given(baseMemberRepository.findById(validCurator.getId())).willReturn(Optional.of(validCurator)); + } + + // welcome 멤버는 normal 멤버로 취급한다. (normalMember 리포지토리에서 탐색하므로) + MemberType welcomeTypeMapping(MemberType memberType) { + if (memberType == MemberType.welcome) { + return MemberType.normal; + } + return memberType; + } + + @ParameterizedTest + @ValueSource(longs = {validMemberId, validCuratorId, validHostId, validWelcomeMemberId}) + @DisplayName("임의의 사용자를 찾는 메서드 테스트") + void getByIdOrThrow(long id) { + // when + var expected = members.get(id); + var actual = baseMemberService.getByIdOrThrow(id); + + // then + assertEquals(expected, actual); + then(baseMemberRepository).should(times(1)).findById(id); + } + + @ParameterizedTest + @ValueSource(longs = {validMemberId, validCuratorId, validHostId, validWelcomeMemberId}) + @DisplayName("임의의 사용자의 타입을 반환하는 메서드 테스트") + void getMemberType(long id) { + // when + var expected = members.get(id).getMemberType(); + var actual = baseMemberService.getMemberType(id); + + // then + assertEquals(expected, actual); + then(baseMemberRepository).should(times(1)).findById(id); + } + + @ParameterizedTest + @ValueSource(longs = {validMemberId, validCuratorId, validHostId, validWelcomeMemberId}) + @DisplayName("각 유형의 사용자를 찾고 응답 반환하는 메서드 테스트") + void findById(long id) { + // given + given(derivedMemberServiceFactory.get(MemberType.normal)).willReturn(mockMemberService); + given(derivedMemberServiceFactory.get(MemberType.host)).willReturn(mockHostService); + given(derivedMemberServiceFactory.get(MemberType.curator)).willReturn(mockCuratorService); + + given(mockMemberService.getMemberResponseById(any())).willReturn(memberResponse); + given(mockHostService.getMemberResponseById(any())).willReturn(memberResponse); + given(mockCuratorService.getMemberResponseById(any())).willReturn(memberResponse); + + // when + baseMemberService.findById(id); + + // then + MemberType mappedType = welcomeTypeMapping(members.get(id).getMemberType()); + then(derivedMemberServiceFactory).should(times(1)).get(mappedType); + then(baseMemberRepository).should(times(1)).findById(id); + } + + @ParameterizedTest + @ValueSource(longs = {validMemberId, validCuratorId, validHostId, validWelcomeMemberId}) + @DisplayName("중복 있는 이메일 있는 경우 테스트") + void checkUniqueEmailOrThrow(long id) { + // given + String email = members.get(id).getEmail(); + Optional<BaseMember> optionalMember = Optional.of(members.get(id)); + given(baseMemberRepository.findByEmail(email)).willReturn(optionalMember); + + // when & then + // TODO: 커스텀 예외 처리 필요 + assertThrows(RuntimeException.class, () -> baseMemberService.checkUniqueEmailOrThrow(email)); + then(baseMemberRepository).should(times(1)).findByEmail(email); + } + + @Test + @DisplayName("고유한 이메일을 찾은 경우 테스트") + void checkUniqueEmailOrThrow_Unique() { + // given + String uniqueEmail = "unique1@example.com"; + given(baseMemberRepository.findByEmail(uniqueEmail)).willReturn(Optional.empty()); + + // when + baseMemberService.checkUniqueEmailOrThrow(uniqueEmail); + + // then + then(baseMemberRepository).should(times(1)).findByEmail(uniqueEmail); + } + + @ParameterizedTest + @ValueSource(longs = {validMemberId, validCuratorId, validHostId, validWelcomeMemberId}) + @DisplayName("각 유형의 사용자를 제거하는 메서드 테스트") + void deleteById(long id) { + // given + given(derivedMemberServiceFactory.get(MemberType.normal)).willReturn(mockMemberService); + given(derivedMemberServiceFactory.get(MemberType.host)).willReturn(mockHostService); + given(derivedMemberServiceFactory.get(MemberType.curator)).willReturn(mockCuratorService); + willDoNothing().given(mockMemberService).deleteById(any()); + willDoNothing().given(mockHostService).deleteById(any()); + willDoNothing().given(mockCuratorService).deleteById(any()); + + // when + baseMemberService.deleteById(id); + + // then + MemberType mappedType = welcomeTypeMapping(members.get(id).getMemberType()); + then(derivedMemberServiceFactory).should(times(1)).get(mappedType); + then(baseMemberRepository).should(times(1)).deleteById(id); + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorServiceUnitTest.java new file mode 100644 index 00000000..735d8c4a --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/CuratorServiceUnitTest.java @@ -0,0 +1,154 @@ +package org.ktc2.cokaen.wouldyouin.member.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.ktc2.cokaen.wouldyouin._global.TestData.ImageDomain.createValidMemberImage; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidCurator; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidMember; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; +import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; +import org.ktc2.cokaen.wouldyouin._common.vo.Area; +import org.ktc2.cokaen.wouldyouin._global.TestUtil; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.edit.CuratorEditRequest; +import org.ktc2.cokaen.wouldyouin.member.persist.BaseMemberRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.Curator; +import org.ktc2.cokaen.wouldyouin.member.persist.CuratorRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.Member; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class CuratorServiceUnitTest { + + @Mock + private CuratorRepository curatorRepository; + @Mock + private MemberRepository memberRepository; + @Mock + private BaseMemberRepository baseMemberRepository; + @Mock + private MemberImageService memberImageService; + + @InjectMocks + private CuratorService curatorService; + + private Curator validCurator; + + @BeforeEach + void setUp() { + validCurator = createValidCurator(); + } + + @Test + @DisplayName("큐레이터 사용자 생성 테스트") + void createCurator() { + // given + Member validMember = createValidMember(); + given(memberRepository.findById(validMember.getId())).willReturn(Optional.of(validMember)); + + // when + curatorService.createCurator(validMember.getId()); + + // then + then(memberRepository).should(times(1)).deleteById(validMember.getId()); + then(baseMemberRepository).should(times(1)).deleteById(validMember.getId()); + then(memberRepository).should(times(1)).flush(); + then(baseMemberRepository).should(times(1)).flush(); + then(curatorRepository).should(times(1)).save(any(Curator.class)); + } + + @Test + @DisplayName("") + void updateCurator() { + // given + Long newProfileImageId = 5L; + MemberImage newProfileImage = createValidMemberImage(newProfileImageId); + CuratorEditRequest editRequest = CuratorEditRequest.curatorEditRequestBuilder() + .nickname(TestUtil.getOrNull("newNickname")) + .phoneNumber(TestUtil.getOrNull("010-1010-8888")) + .profileImageId(TestUtil.getOrNull(newProfileImageId)) + .area(Area.광주) + .intro(TestUtil.getOrNull("new intro")) + .build(); + + given(curatorRepository.findById(validCurator.getId())).willReturn(Optional.of(validCurator)); + given(memberImageService.getById(newProfileImageId)).willReturn(newProfileImage); + + // when + curatorService.updateCurator(validCurator.getId(), editRequest); + + // then + then(curatorRepository).should(times(1)).findById(validCurator.getId()); + int times = 1; + if (editRequest.getProfileImageId() == null) { + times = 0; + } + then(memberImageService).should(times(times)).getById(newProfileImageId); + } + + @Test + @DisplayName("큐레이터 삭제 테스트") + void deleteById() { + // given + given(curatorRepository.findById(validCurator.getId())).willReturn(Optional.of(validCurator)); + + // when + curatorService.deleteById(validCurator.getId()); + + // then + then(curatorRepository).should(times(1)).delete(validCurator); + } + + @Test + @DisplayName("id로 사용자 응답 생성 테스트") + void getMemberResponseById() { + // given + given(curatorRepository.findById(validCurator.getId())).willReturn(Optional.of(validCurator)); + + // when + curatorService.getMemberResponseById(validCurator.getId()); + + // then + then(curatorRepository).should(times(1)).findById(validCurator.getId()); + } + + @Test + @DisplayName("id로 큐레이터 얻어오기 테스트") + void getByIdOrThrow() { + // given + given(curatorRepository.findById(validCurator.getId())).willReturn(Optional.of(validCurator)); + + // when + curatorService.getByIdOrThrow(validCurator.getId()); + + // then + then(curatorRepository).should(times(1)).findById(validCurator.getId()); + } + + @Test + @DisplayName("대상 사용자 유형 확인 테스트") + void getTargetMemberType() { + assertThat(curatorService.getTargetMemberType()).isEqualTo(MemberType.curator); + } + + @Test + @DisplayName("LikeableMemberService 얻어오기 테스트") + void getLikeableMemberService() { + assertThat(curatorService.getLikeableMemberService()).isEqualTo(curatorService); + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/HostServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/HostServiceUnitTest.java new file mode 100644 index 00000000..a4052dd2 --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/HostServiceUnitTest.java @@ -0,0 +1,185 @@ +package org.ktc2.cokaen.wouldyouin.member.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidHost; +import static org.ktc2.cokaen.wouldyouin._global.TestData.ImageDomain.createValidMemberImage; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.times; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; +import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; +import org.ktc2.cokaen.wouldyouin.auth.application.dto.LocalLoginRequest; +import org.ktc2.cokaen.wouldyouin._global.TestUtil; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.create.HostCreateRequest; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.edit.HostEditRequest; +import org.ktc2.cokaen.wouldyouin.member.persist.Host; +import org.ktc2.cokaen.wouldyouin.member.persist.HostRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class HostServiceUnitTest { + + @Mock + private HostRepository hostRepository; + @Mock + private PasswordEncoder passwordEncoder; + @Mock + private MemberImageService memberImageService; + + @Mock + private HostCreateRequest hostCreateRequest; + + @InjectMocks + private HostService hostService; + + private Host validHost; + + @BeforeEach + void setUp() { + validHost = createValidHost(); + } + + @Test + @DisplayName("호스트 사용자 생성 테스트") + void createHost() { + // given + String password = "host0password"; + String hashedPassword = validHost.getHashedPassword(); + MemberImage profileImage = validHost.getProfileImage(); + Long profileImageId = profileImage.getId(); + + given(hostCreateRequest.getPassword()).willReturn(password); + given(hostCreateRequest.getProfileImageId()).willReturn(profileImageId); + given(passwordEncoder.encode(password)).willReturn(hashedPassword); + given(memberImageService.getById(profileImageId)).willReturn(profileImage); + given(hostCreateRequest.toEntity(hashedPassword, profileImage)).willReturn(validHost); + given(hostRepository.save(validHost)).willReturn(validHost); + + // when + hostService.createHost(hostCreateRequest); + + // then + var ignore1 = then(hostCreateRequest).should(times(1)).getPassword(); + var ignore2 = then(hostCreateRequest).should(times(1)).getProfileImageId(); + then(passwordEncoder).should(times(1)).encode(password); + then(memberImageService).should(times(1)).getById(profileImageId); + then(hostCreateRequest).should(times(1)) + .toEntity(hashedPassword, profileImage); + then(hostRepository).should(times(1)).save(validHost); + then(memberImageService).should(times(1)).setBaseMember(profileImage, validHost); + } + + @Test + @DisplayName("호스트 업데이트 테스트") + void updateHost() { + // given + Long newProfileImageId = 5L; + MemberImage newProfileImage = createValidMemberImage(newProfileImageId); + newProfileImage.setBaseMember(validHost); + HostEditRequest editRequest = HostEditRequest.builder() + .nickname(TestUtil.getOrNull("newNickname")) + .phoneNumber(TestUtil.getOrNull("010-1010-8888")) + .profileImageId(TestUtil.getOrNull(newProfileImageId)) + .intro(TestUtil.getOrNull("new intro")) + .hashtag(TestUtil.getOrNull("#new#hashtag")) + .build(); + + given(hostRepository.findById(validHost.getId())).willReturn(Optional.of(validHost)); + given(memberImageService.getById(newProfileImageId)).willReturn(newProfileImage); + + // when + hostService.updateHost(validHost.getId(), editRequest); + + // then + then(hostRepository).should(times(1)).findById(validHost.getId()); + int times = 1; + if (editRequest.getProfileImageId() == null) { + times = 0; + } + then(memberImageService).should(times(times)).getById(newProfileImageId); + } + + @Test + @DisplayName("호스트 삭제 테스트") + void deleteById() { + // given + given(hostRepository.findById(validHost.getId())).willReturn(Optional.of(validHost)); + + // when + hostService.deleteById(validHost.getId()); + + // then + then(hostRepository).should(times(1)).delete(validHost); + } + + @Test + @DisplayName("id로 사용자 응답 생성 테스트") + void getMemberResponseById() { + // given + given(hostRepository.findById(validHost.getId())).willReturn(Optional.of(validHost)); + + // when + hostService.getMemberResponseById(validHost.getId()); + + // then + then(hostRepository).should(times(1)).findById(validHost.getId()); + } + + @Test + @DisplayName("로그인 요청으로 사용자 응답 테스트") + void getMemberResponseBy() { + // given + String password = "host0password"; + LocalLoginRequest loginRequest = new LocalLoginRequest(validHost.getEmail(), password); + given(passwordEncoder.encode(password)).willReturn(validHost.getHashedPassword()); + given(hostRepository.findByEmailAndHashedPassword( + validHost.getEmail(), validHost.getHashedPassword())) + .willReturn(Optional.of(validHost)); + + // when + hostService.getMemberResponseBy(loginRequest); + + // then + then(passwordEncoder).should(times(1)).encode(password); + then(hostRepository).should(times(1)).findByEmailAndHashedPassword( + validHost.getEmail(), validHost.getHashedPassword()); + } + + @Test + @DisplayName("id로 호스트 얻어오기 테스트") + void getByIdOrThrow() { + // given + given(hostRepository.findById(validHost.getId())).willReturn(Optional.of(validHost)); + + // when + hostService.getByIdOrThrow(validHost.getId()); + + // then + then(hostRepository).should(times(1)).findById(validHost.getId()); + } + + @Test + @DisplayName("대상 사용자 유형 확인 테스트") + void getTargetMemberType() { + assertThat(hostService.getTargetMemberType()).isEqualTo(MemberType.host); + } + + @Test + @DisplayName("LikeableMemberService 얻어오기 테스트") + void getLikeableMemberService() { + assertThat(hostService.getLikeableMemberService()).isEqualTo(hostService); + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/MemberServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/MemberServiceUnitTest.java new file mode 100644 index 00000000..038ec16b --- /dev/null +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/member/application/MemberServiceUnitTest.java @@ -0,0 +1,199 @@ +package org.ktc2.cokaen.wouldyouin.member.application; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.ktc2.cokaen.wouldyouin._global.TestData.ImageDomain.createValidMemberImage; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidMember; +import static org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain.createValidWelcomeMember; +import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; + +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.ktc2.cokaen.wouldyouin.Image.application.MemberImageService; +import org.ktc2.cokaen.wouldyouin.Image.persist.MemberImage; +import org.ktc2.cokaen.wouldyouin._global.TestUtil; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.MemberAdditionalInfoRequest; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.create.MemberCreateRequest; +import org.ktc2.cokaen.wouldyouin.member.application.dto.request.edit.MemberEditRequest; +import org.ktc2.cokaen.wouldyouin.member.persist.Member; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberRepository; +import org.ktc2.cokaen.wouldyouin.member.persist.MemberType; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class MemberServiceUnitTest { + + @Mock + private MemberRepository memberRepository; + @Mock + MemberImageService memberImageService; + + @Mock + MemberCreateRequest memberCreateRequest; + @Mock + MemberEditRequest memberEditRequest; + @Mock + MemberAdditionalInfoRequest memberAdditionalInfoRequest; + + @InjectMocks + private MemberService memberService; + + private Member validMember; + + @BeforeEach + void setUp() { + validMember = createValidMember(); + } + + @Test + @DisplayName("일반 사용자 생성 테스트") + void createMember() { + // given + given(memberCreateRequest.getProfileImageUrl()).willReturn(validMember.getProfileImageUrl()); + given(memberCreateRequest.toEntity(validMember.getProfileImage())).willReturn(validMember); + given(memberRepository.save(validMember)).willReturn(validMember); + given(memberImageService.convert(memberCreateRequest.getProfileImageUrl())).willReturn(validMember.getProfileImage()); + + // when + memberService.createMember(memberCreateRequest); + + // then + then(memberRepository).should(times(1)).save(validMember); + } + + @Test + @DisplayName("사용자 업데이트 테스트") + void updateMember() { + // given + MemberImage validMemberImage = createValidMemberImage(5L); + Long givenMemberId = validMember.getId(); + given(memberRepository.findById(givenMemberId)).willReturn(Optional.of(validMember)); + given(memberImageService.getById(validMember.getProfileImage().getId())).willReturn(validMemberImage); + + // editRequest의 각 필드는 값을 가지거나 null임 + given(memberEditRequest.getNickname()).willReturn(TestUtil.getOrNull(validMember.getNickname())); + given(memberEditRequest.getArea()).willReturn(TestUtil.getOrNull(validMember.getArea())); + given(memberEditRequest.getPhoneNumber()).willReturn(TestUtil.getOrNull(validMember.getPhone())); + given(memberEditRequest.getProfileImageId()).willReturn(TestUtil.getOrNull(validMember.getId())); + + // when + memberService.updateMember(givenMemberId, memberEditRequest); + + // then + then(memberRepository).should(times(1)).findById(validMember.getId()); + var ignore1 = then(memberEditRequest).should(times(1)).getNickname(); + var ignore2 = then(memberEditRequest).should(times(1)).getArea(); + var ignore3 = then(memberEditRequest).should(times(1)).getPhoneNumber(); + var ignore4 = then(memberEditRequest).should(times(1)).getProfileImageId(); + } + + @Test + @DisplayName("소셜 신규 사용자 추가정보 기입 테스트") + void updateWelcomeMember() { + Member validWelcomeMember = createValidWelcomeMember(); + given(memberRepository.findById(validWelcomeMember.getId())).willReturn(Optional.of(validWelcomeMember)); + given(memberAdditionalInfoRequest.getPhone()).willReturn(validWelcomeMember.getPhone()); + given(memberAdditionalInfoRequest.getArea()).willReturn(validWelcomeMember.getArea()); + given(memberAdditionalInfoRequest.getGender()).willReturn(validWelcomeMember.getGender()); + + // when + memberService.updateWelcomeMember(validWelcomeMember.getId(), memberAdditionalInfoRequest); + + // then + then(memberRepository).should(times(1)).findById(validWelcomeMember.getId()); + var ignore1 = then(memberAdditionalInfoRequest).should(times(1)).getPhone(); + var ignore2 = then(memberAdditionalInfoRequest).should(times(1)).getArea(); + var ignore3 = then(memberAdditionalInfoRequest).should(times(1)).getGender(); + } + + @Test + @DisplayName("소셜 신규 사용자 추가정보 기입 - welcome 유형 아닌 사용자 사용 테스트") + void updateWelcomeMember_NormalMemberUsage() { + given(memberRepository.findById(validMember.getId())).willReturn(Optional.of(validMember)); + + // when & then + // TODO: 커스텀 예외 필요 + assertThrows(RuntimeException.class, () -> + memberService.updateWelcomeMember(validMember.getId(), memberAdditionalInfoRequest)); + + // then + then(memberRepository).should(times(1)).findById(validMember.getId()); + var ignore1 = then(memberAdditionalInfoRequest).should(never()).getPhone(); + var ignore2 = then(memberAdditionalInfoRequest).should(never()).getArea(); + var ignore3 = then(memberAdditionalInfoRequest).should(never()).getGender(); + } + + + @Test + @DisplayName("사용자 삭제 테스트") + void deleteById() { + // given + Long idToDelete = validMember.getId(); + given(memberRepository.findById(idToDelete)).willReturn(Optional.of(validMember)); + + // when + memberService.deleteById(idToDelete); + + // then + then(memberRepository).should(times(1)).findById(validMember.getId()); + then(memberRepository).should(times(1)).delete(validMember); + } + + @Test + @DisplayName("id로 사용자 응답 생성 테스트") + void getMemberResponseById() { + // given + Long idToGet = validMember.getId(); + given(memberRepository.findById(idToGet)).willReturn(Optional.of(validMember)); + + // when + memberService.getMemberResponseById(idToGet); + + // then + then(memberRepository).should(times(1)).findById(validMember.getId()); + } + + @Test + @DisplayName("id로 엔티티 얻어오기 테스트") + void getByIdOrThrow() { + // given + Long idToGet = validMember.getId(); + given(memberRepository.findById(idToGet)).willReturn(Optional.of(validMember)); + + // when + memberService.getByIdOrThrow(idToGet); + + // then + then(memberRepository).should(times(1)).findById(validMember.getId()); + } + + @Test + @DisplayName("소셜 id로 엔티티 얻어오기 테스트") + void getMemberIdentifierBySocialId() { + // given + given(memberRepository.findBySocialId(validMember.getSocialId())).willReturn(Optional.of(validMember)); + + // when + memberService.getMemberIdentifierBySocialId(validMember.getSocialId()); + + // then + then(memberRepository).should(times(1)).findBySocialId(validMember.getSocialId()); + } + + @Test + @DisplayName("대상 사용자 유형 확인테스트") + void getTargetMemberType() { + assertThat(memberService.getTargetMemberType()).isEqualTo(MemberType.normal); + } +} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationControllerUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationControllerUnitTest.java index f7199c35..7b9124ff 100644 --- a/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationControllerUnitTest.java +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationControllerUnitTest.java @@ -1,119 +1,140 @@ -package org.ktc2.cokaen.wouldyouin.reservation; - -import static java.lang.Math.abs; -import static org.mockito.Mockito.verify; -import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf; -import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import java.util.Random; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.ktc2.cokaen.wouldyouin.auth.application.JwtAuthFilter; -import org.ktc2.cokaen.wouldyouin.global.TestData; -import org.ktc2.cokaen.wouldyouin.member.application.MemberService; -import org.ktc2.cokaen.wouldyouin.reservation.api.ReservationController; -import org.ktc2.cokaen.wouldyouin.reservation.application.ReservationService; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; -import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.http.MediaType; -import org.springframework.security.test.context.support.WithMockUser; -import org.springframework.test.web.servlet.MockMvc; -import org.springframework.test.web.servlet.setup.MockMvcBuilders; -import org.springframework.web.context.WebApplicationContext; - -@WithMockUser(username = "user", roles = {"USER"}) -@WebMvcTest(ReservationController.class) -class ReservationControllerUnitTest { - - @MockBean - private ReservationService reservationService; - - @MockBean - private MemberService memberService; - - @MockBean - private JwtAuthFilter jwtAuthFilter; - - @Autowired - private MockMvc mockMvc; - - @Autowired - private WebApplicationContext context; - - private static Long id; - - private static ObjectMapper objectMapper; - - - @BeforeEach - public void setup() throws Exception { - mockMvc = MockMvcBuilders - .webAppContextSetup(context) - .apply(springSecurity()) - .build(); - } - - @BeforeAll - public static void init() { - id = abs(new Random().nextLong()); - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - } - - @Test - @DisplayName("모든 예약 조회 - 성공") - void getReservations() throws Exception { - mockMvc.perform(get("/api/reservations")).andExpect(status().isOk()); - verify(reservationService).getAll(); - } - - @Test - @DisplayName("예약 조회 - 성공") - void getReservation() throws Exception { - mockMvc.perform(get("/api/reservations/" + id)).andExpect(status().isOk()); - verify(reservationService).getById(id); - } - - @Test - @DisplayName("사용자 id를 통한 예약 조회 - 성공") - void getReservationsByMemberId() throws Exception { - mockMvc.perform(get("/api/reservations/members/" + id)).andExpect(status().isOk()); - verify(reservationService).getAllByMemberId(id); - - } - - @Test - @DisplayName("행사 id를 통한 예약 조회 - 성공") - void getReservationsByEventId() throws Exception { - mockMvc.perform(get("/api/reservations/events/" + id)).andExpect(status().isOk()); - verify(reservationService).getAllByEventId(id); - } - - @Test - @DisplayName("예약 생성 - 성공") - void createReservation() throws Exception { - mockMvc.perform(post("/api/reservations") - .with(csrf()) - .contentType(MediaType.APPLICATION_JSON) - .content(objectMapper.writeValueAsString(TestData.validReservationRequest))) - .andExpect(status().isCreated()); - } - - @Test - @DisplayName("예약 삭제 - 성공") - void deleteReservation() throws Exception { - mockMvc.perform(delete("/api/reservations/" + id) - .with(csrf())) - .andExpect(status().isNoContent()); - verify(reservationService).delete(id); - } -} \ No newline at end of file +//package org.ktc2.cokaen.wouldyouin.reservation; +// +//import static java.lang.Math.abs; +//import static org.mockito.ArgumentMatchers.eq; +//import static org.mockito.BDDMockito.then; +//import static org.mockito.Mockito.times; +//import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity; +//import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +//import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; +//import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +// +//import com.fasterxml.jackson.databind.ObjectMapper; +//import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +//import java.util.Random; +//import org.junit.jupiter.api.BeforeAll; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.ktc2.cokaen.wouldyouin._global.TestData.MemberDomain; +//import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockCurator; +//import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockHost; +//import org.ktc2.cokaen.wouldyouin._global.mockMember.WithMockMember; +//import org.ktc2.cokaen.wouldyouin.auth.application.JwtAuthFilter; +//import org.ktc2.cokaen.wouldyouin.reservation.api.ReservationController; +//import org.ktc2.cokaen.wouldyouin.reservation.application.ReservationService; +//import org.springframework.beans.factory.annotation.Autowired; +//import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +//import org.springframework.boot.test.mock.mockito.MockBean; +//import org.springframework.data.domain.PageRequest; +//import org.springframework.test.web.servlet.MockMvc; +//import org.springframework.test.web.servlet.setup.MockMvcBuilders; +//import org.springframework.web.context.WebApplicationContext; +// +//@WebMvcTest(ReservationController.class) +//class ReservationControllerUnitTest { +// +// private static ObjectMapper objectMapper; +// +// @MockBean +// private ReservationService reservationService; +// +// @MockBean +// private JwtAuthFilter jwtAuthFilter; +// +// @Autowired +// private MockMvc mockMvc; +// +// @Autowired +// private WebApplicationContext context; +// private static final long randomId = abs(new Random().nextLong()); +// +// @BeforeAll +// public static void init() { +// objectMapper = new ObjectMapper(); +// objectMapper.registerModule(new JavaTimeModule()); +// } +// +// @BeforeEach +// public void setup() throws Exception { +// mockMvc = MockMvcBuilders +// .webAppContextSetup(context) +// .apply(springSecurity()) +// .build(); +// } +// +// @Test +// @DisplayName("jwt 토큰 정보에 해당하는 member의 예약 목록을 조회한다.") +// @WithMockMember +// void getReservationsByMemberId1() throws Exception { +// // given, when +// mockMvc.perform(get("/api/reservations") +// .param("page", "5") +// .param("size", "20") +// .param("lastId", "100")).andDo(print()) +// .andExpect(status().isOk()); +// +// // then +// then(reservationService).should(times(1)).getAllByMemberId( +// eq(MemberDomain.validMemberId), eq(PageRequest.of(5, 20)), eq(100L)); +// } +// +// @Test +// @DisplayName("RequestParam으로 페이지 정보가 주어지지 않으면 기본값으로 페이지를 생성한다") +// @WithMockMember +// void getReservationsByMemberId2() throws Exception { +// // given, when +// mockMvc.perform(get("/api/reservations")).andDo(print()) +// .andExpect(status().isOk()); +// +// // then +// then(reservationService).should(times(1)).getAllByMemberId( +// eq(MemberDomain.validMemberId), eq(PageRequest.of(0, 10)), eq(Long.MAX_VALUE)); +// } +// +// @Test +// @DisplayName("jwt 토큰 정보에 해당하는 curator의 예약 목록을 조회한다.") +// @WithMockCurator +// void getReservationsByMemberId3() throws Exception { +// // given, when +// mockMvc.perform(get("/api/reservations") +// .param("page", "5") +// .param("size", "20") +// .param("lastId", "100")).andDo(print()) +// .andExpect(status().isOk()); +// +// // then +// then(reservationService).should(times(1)).getAllByMemberId( +// eq(MemberDomain.validMemberId), eq(PageRequest.of(5, 20)), eq(100L)); +// } +// +// @Test +// @DisplayName("Host의 권한으로 자신의 예약을 조회할 수 없다.") +// @WithMockHost +// void getReservationsByMemberId4() throws Exception { +// // given, when +// mockMvc.perform(get("/api/reservations") +// .param("page", "5") +// .param("size", "20") +// .param("lastId", "100")).andDo(print()) +// .andExpect(status().isUnauthorized()); +// +// // then +// then(reservationService).shouldHaveNoInteractions(); +// } +// +// @Test +// @DisplayName("PathVariable로 이벤트 ID를 받아 해당하는 이벤트를 조회한다.") +// @WithMockHost +// void getReservationsByMemberId4() throws Exception { +// // given, when +// mockMvc.perform(get("/api/reservations") +// .param("page", "5") +// .param("size", "20") +// .param("lastId", "100")).andDo(print()) +// .andExpect(status().isUnauthorized()); +// +// // then +// then(reservationService).shouldHaveNoInteractions(); +// } +//} \ No newline at end of file diff --git a/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationServiceUnitTest.java b/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationServiceUnitTest.java index c73601de..86fa090a 100644 --- a/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationServiceUnitTest.java +++ b/src/test/java/org/ktc2/cokaen/wouldyouin/reservation/ReservationServiceUnitTest.java @@ -1,109 +1,101 @@ -package org.ktc2.cokaen.wouldyouin.reservation; - -import static java.lang.Math.abs; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -import java.util.List; -import java.util.Optional; -import java.util.Random; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.ktc2.cokaen.wouldyouin.event.application.EventService; -import org.ktc2.cokaen.wouldyouin.global.TestData; -import org.ktc2.cokaen.wouldyouin.member.application.MemberService; -import org.ktc2.cokaen.wouldyouin.payment.application.PaymentService; -import org.ktc2.cokaen.wouldyouin.reservation.application.ReservationService; -import org.ktc2.cokaen.wouldyouin.reservation.persist.ReservationRepository; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; - -@ExtendWith({MockitoExtension.class}) -class ReservationServiceUnitTest { - - private ReservationService reservationService; - - @Mock - private ReservationRepository reservationRepository; - @Mock - private MemberService memberService; - @Mock - private EventService eventService; - @Mock - private PaymentService paymentService; - - private Long id; - - @BeforeEach - void setUp() { - reservationService = new ReservationService(reservationRepository, paymentService, memberService, eventService); - id = abs(new Random().nextLong()); - } - - @Test - @DisplayName("모든 예약 조회 - 성공") - void getAll() { - when(reservationRepository.findAll()).thenReturn(List.of()); - reservationService.getAll(); - verify(reservationRepository, times(1)).findAll(); - } - - @Test - @DisplayName("사용자 id를 통한 모든 예약 조회 - 성공") - void getAllByMemberId() { - when(reservationRepository.findByMemberId(id)).thenReturn(List.of()); - reservationService.getAllByMemberId(id); - verify(reservationRepository, times(1)).findByMemberId(id); - } - - @Test - @DisplayName("행사 id를 통한 모든 예약 조회 - 성공") - void getAllByEventId() { - when(reservationRepository.findByEventId(id)).thenReturn(List.of()); - reservationService.getAllByEventId(id); - verify(reservationRepository, times(1)).findByEventId(id); - } - - @Test - @DisplayName("예약 id를 통한 모든 예약 조회 - 성공") - void getById() { - when(reservationRepository.findById(id)).thenReturn(Optional.of(TestData.validReservation)); - reservationService.getById(id); - verify(reservationRepository, times(1)).findById(id); - } - - @Test - @DisplayName("유효하지 않은 예약 id를 통한 모든 예약 조회 - 실패") - void getByInvalidId() { - when(reservationRepository.findById(id)).thenThrow(RuntimeException.class); - assertThrows(RuntimeException.class, () -> reservationService.getById(id)); - } - - @Test - @DisplayName("예약 생성 - 성공") - void create() { - when(reservationRepository.save(any())).thenReturn(TestData.validReservation); - reservationService.create(1L, TestData.validReservationRequest); - verify(reservationRepository, times(1)).save(any()); - } - - @Test - @DisplayName("예약 삭제 - 성공") - void delete() { - when(reservationRepository.findById(id)).thenReturn(Optional.of(TestData.validReservation)); - reservationService.delete(id); - verify(reservationRepository, times(1)).findById(id); - } - - @Test - @DisplayName("예약 삭제 - 실패") - void deleteByInvalidId() { - when(reservationRepository.findById(id)).thenThrow(RuntimeException.class); - assertThrows(RuntimeException.class, () -> reservationService.delete(id)); - } -} \ No newline at end of file +//package org.ktc2.cokaen.wouldyouin.reservation; +// +//import static java.lang.Math.abs; +//import static org.junit.jupiter.api.Assertions.assertThrows; +//import static org.mockito.ArgumentMatchers.any; +//import static org.mockito.Mockito.times; +//import static org.mockito.Mockito.verify; +//import static org.mockito.Mockito.when; +// +//import java.util.List; +//import java.util.Optional; +//import java.util.Random; +//import org.junit.jupiter.api.BeforeEach; +//import org.junit.jupiter.api.DisplayName; +//import org.junit.jupiter.api.Test; +//import org.junit.jupiter.api.extension.ExtendWith; +//import org.ktc2.cokaen.wouldyouin.event.application.EventService; +//import org.ktc2.cokaen.wouldyouin.global.TestData; +//import org.ktc2.cokaen.wouldyouin.member.application.MemberService; +//import org.ktc2.cokaen.wouldyouin.payment.application.PaymentService; +//import org.ktc2.cokaen.wouldyouin.reservation.application.ReservationService; +//import org.ktc2.cokaen.wouldyouin.reservation.persist.ReservationRepository; +//import org.mockito.Mock; +//import org.mockito.junit.jupiter.MockitoExtension; +// +//@ExtendWith({MockitoExtension.class}) +//class ReservationServiceUnitTest { +// +// private ReservationService reservationService; +// +// @Mock +// private ReservationRepository reservationRepository; +// @Mock +// private MemberService memberService; +// @Mock +// private EventService eventService; +// @Mock +// private PaymentService paymentService; +// +// private Long id; +// +// @BeforeEach +// void setUp() { +// reservationService = new ReservationService(reservationRepository, paymentService, memberService, eventService); +// id = abs(new Random().nextLong()); +// } +// +// @Test +// @DisplayName("사용자 id를 통한 모든 예약 조회 - 성공") +// void getAllByMemberId() { +// when(reservationRepository.findByMemberIdOrderByReservationIdDesc(id)).thenReturn(List.of()); +// reservationService.getAllByMemberId(id); +// verify(reservationRepository, times(1)).findByMemberIdOrderByReservationIdDesc(id); +// } +// +// @Test +// @DisplayName("행사 id를 통한 모든 예약 조회 - 성공") +// void getAllByEventId() { +// when(reservationRepository.findByEventIdOrderByReservationIdDesc(id)).thenReturn(List.of()); +// reservationService.getAllByEventId(id); +// verify(reservationRepository, times(1)).findByEventIdOrderByReservationIdDesc(id); +// } +// +// @Test +// @DisplayName("예약 id를 통한 예약 조회 - 성공") +// void getById() { +// when(reservationRepository.findById(id)).thenReturn(Optional.of(TestData.validReservation)); +// reservationService.getById(id); +// verify(reservationRepository, times(1)).findById(id); +// } +// +// @Test +// @DisplayName("유효하지 않은 예약 id를 통한 모든 예약 조회 - 실패") +// void getByInvalidId() { +// when(reservationRepository.findById(id)).thenThrow(RuntimeException.class); +// assertThrows(RuntimeException.class, () -> reservationService.getById(id)); +// } +// +// @Test +// @DisplayName("예약 생성 - 성공") +// void create() { +// when(reservationRepository.save(any())).thenReturn(TestData.validReservation); +// reservationService.create(1L, TestData.validReservationRequest); +// verify(reservationRepository, times(1)).save(any()); +// } +// +// @Test +// @DisplayName("예약 삭제 - 성공") +// void delete() { +// when(reservationRepository.findById(id)).thenReturn(Optional.of(TestData.validReservation)); +// reservationService.delete(id); +// verify(reservationRepository, times(1)).findById(id); +// } +// +// @Test +// @DisplayName("예약 삭제 - 실패") +// void deleteByInvalidId() { +// when(reservationRepository.findById(id)).thenThrow(RuntimeException.class); +// assertThrows(RuntimeException.class, () -> reservationService.delete(id)); +// } +//} \ No newline at end of file