From 76697a1ca5500dd250b0eedfe367be133268729e Mon Sep 17 00:00:00 2001 From: Parkjyun <98092394+Parkjyun@users.noreply.github.com> Date: Wed, 17 Jul 2024 08:59:46 +0900 Subject: [PATCH] =?UTF-8?q?[feat]=20=EC=8B=9D=EB=8B=B9=20=EC=A0=9C?= =?UTF-8?q?=EB=B3=B4=20api=20=EC=83=9D=EC=84=B1=20(#92)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [dependency] add AWS dependency * [feat] configure AwsConfig.java * [feat] create S3Service.java * [fix] add address in Store.java * [feat] create dtos for post store api * [fix] change name of dtos used for heart post and delete * [fix] change controller due to changed dto name * [fix] add bucket-endpoint in when uploading image * [fix] add validation in Store post request * [feat] add all args constructor in Point.java * [feat] add static factory method in entities * [feat] add method for checking existence of store by latitude and longitude in StoreRepository.java * [feat] add method for checking store existence in StoreFinder.java * [feat] add method for finding university in UniversityFinder.java * [feat] create MenuUpdater * [feat] create ReportUpdater * [feat] create StoreImageUpdater * [feat] create StoreUpdater * [feat] create UniversityStoreUpdater * [fix] add validation in MenuPostRequest.java * [feat] create University error code * [feat] add error codes in StoreErrorCode.java * [feat] create command for Store post api * [feat] add exceptions in GlobalExceptionHandler.java * [feat] create store post logic * [feat] create post store api * [refac] add final keyword in method parameter * [refac] fix menu create method's signature * [refac] add toEntityMethod in StorePostCommand.java * [refac] add final keyword in StorePostResponse.java * [refac] delete unused static factory method in Store.java --- build.gradle | 4 + .../api/advice/GlobalExceptionHandler.java | 17 +++- .../api/favorite/service/FavoriteUpdater.java | 5 +- .../api/menu/service/MenuFinder.java | 2 +- .../api/menu/service/MenuUpdater.java | 19 ++++ .../api/report/service/ReportUpdater.java | 17 ++++ .../api/store/controller/StoreController.java | 28 +++++- .../controller/request/MenuPostRequest.java | 13 +++ .../controller/request/StorePostRequest.java | 19 ++++ .../store/service/HeartCommandService.java | 16 ++-- .../store/service/StoreCommandService.java | 90 +++++++++++++++++++ .../api/store/service/StoreFinder.java | 3 + .../api/store/service/StoreImageUpdater.java | 17 ++++ .../api/store/service/StoreUpdater.java | 17 ++++ .../service/command/HeartDeleteCommand.java | 11 +++ .../service/command/HeartPostCommand.java | 10 +++ .../service/command/StoreDeleteCommand.java | 11 --- .../service/command/StorePostCommand.java | 44 ++++++++- .../service/response/StorePostResponse.java | 12 +++ .../university/service/UniversityFinder.java | 8 +- .../service/UniversityStoreUpdater.java | 17 ++++ .../common/code/StoreErrorCode.java | 4 +- .../common/code/UniversityErrorCode.java | 16 ++++ .../hankkiserver/domain/common/Point.java | 6 +- .../hankkiserver/domain/menu/model/Menu.java | 15 ++++ .../domain/report/model/Report.java | 15 ++++ .../domain/store/model/Store.java | 32 +++++-- .../domain/store/model/StoreImage.java | 13 +++ .../store/repository/StoreRepository.java | 2 + .../model/UniversityStore.java | 14 +++ .../hankkiserver/external/s3/AwsConfig.java | 47 ++++++++++ .../hankkiserver/external/s3/S3Service.java | 82 +++++++++++++++++ 32 files changed, 586 insertions(+), 40 deletions(-) create mode 100644 src/main/java/org/hankki/hankkiserver/api/menu/service/MenuUpdater.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/report/service/ReportUpdater.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/controller/request/MenuPostRequest.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/controller/request/StorePostRequest.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/StoreCommandService.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/StoreImageUpdater.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/StoreUpdater.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartDeleteCommand.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartPostCommand.java delete mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/command/StoreDeleteCommand.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePostResponse.java create mode 100644 src/main/java/org/hankki/hankkiserver/api/universitystore/service/UniversityStoreUpdater.java create mode 100644 src/main/java/org/hankki/hankkiserver/common/code/UniversityErrorCode.java create mode 100644 src/main/java/org/hankki/hankkiserver/external/s3/AwsConfig.java create mode 100644 src/main/java/org/hankki/hankkiserver/external/s3/S3Service.java diff --git a/build.gradle b/build.gradle index d92528d1..38cfa7b2 100644 --- a/build.gradle +++ b/build.gradle @@ -58,6 +58,10 @@ dependencies { // Redis implementation 'org.springframework.boot:spring-boot-starter-data-redis' + + // AWS + implementation("software.amazon.awssdk:s3:2.21.0") + implementation("software.amazon.awssdk:bom:2.21.0") } tasks.named('test') { diff --git a/src/main/java/org/hankki/hankkiserver/api/advice/GlobalExceptionHandler.java b/src/main/java/org/hankki/hankkiserver/api/advice/GlobalExceptionHandler.java index 3849e82b..0b6f86b4 100644 --- a/src/main/java/org/hankki/hankkiserver/api/advice/GlobalExceptionHandler.java +++ b/src/main/java/org/hankki/hankkiserver/api/advice/GlobalExceptionHandler.java @@ -3,13 +3,16 @@ import lombok.extern.slf4j.Slf4j; import org.hankki.hankkiserver.api.dto.HankkiResponse; import org.hankki.hankkiserver.common.code.BusinessErrorCode; +import org.hankki.hankkiserver.common.code.StoreErrorCode; import org.hankki.hankkiserver.common.exception.BadRequestException; import org.hankki.hankkiserver.common.exception.ConflictException; import org.hankki.hankkiserver.common.exception.NotFoundException; import org.hankki.hankkiserver.common.exception.UnauthorizedException; +import org.springframework.web.bind.MethodArgumentNotValidException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; +import org.springframework.web.multipart.MaxUploadSizeExceededException; @RestControllerAdvice @Slf4j @@ -45,9 +48,21 @@ public HankkiResponse handleMissingServletRequestParameterException(Missin return HankkiResponse.fail(BusinessErrorCode.BAD_REQUEST); } + @ExceptionHandler(MaxUploadSizeExceededException.class) + public HankkiResponse handleMaxUploadSizeExceededException(MaxUploadSizeExceededException e) { + log.error("handleMaxUploadSizeExceededException() in GlobalExceptionHandler throw MaxUploadSizeExceededException : {}", e.getMessage()); + return HankkiResponse.fail(StoreErrorCode.STORE_FILE_SIZE_EXCEEDED); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + public HankkiResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.error("handleMethodArgumentNotValidException() in GlobalExceptionHandler throw MethodArgumentNotValidException : {}", e.getMessage()); + return HankkiResponse.fail(BusinessErrorCode.BAD_REQUEST); + } + @ExceptionHandler(Exception.class) public HankkiResponse handleException(Exception e) { - log.error("handleException() in GlobalExceptionHandler throw [{}] : {}, {}", e.getClass(), e.getMessage(), e.getStackTrace()); + log.error("handleException() in GlobalExceptionHandler throw Exception [{}] : {}", e.getClass() , e.getMessage()); return HankkiResponse.fail(BusinessErrorCode.INTERNAL_SERVER_ERROR); } diff --git a/src/main/java/org/hankki/hankkiserver/api/favorite/service/FavoriteUpdater.java b/src/main/java/org/hankki/hankkiserver/api/favorite/service/FavoriteUpdater.java index 3f771d7a..7ac3ec9a 100644 --- a/src/main/java/org/hankki/hankkiserver/api/favorite/service/FavoriteUpdater.java +++ b/src/main/java/org/hankki/hankkiserver/api/favorite/service/FavoriteUpdater.java @@ -1,9 +1,6 @@ package org.hankki.hankkiserver.api.favorite.service; -import java.util.List; import lombok.RequiredArgsConstructor; -import org.hankki.hankkiserver.common.code.BusinessErrorCode; -import org.hankki.hankkiserver.common.exception.InternalServerException; import org.hankki.hankkiserver.domain.favorite.model.Favorite; import org.hankki.hankkiserver.domain.favorite.repository.FavoriteRepository; import org.springframework.stereotype.Component; @@ -14,7 +11,7 @@ public class FavoriteUpdater { private final FavoriteRepository favoriteRepository; - protected Long save(Favorite favorite) { + protected Long save(final Favorite favorite) { return favoriteRepository.save(favorite).getId(); } } diff --git a/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuFinder.java b/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuFinder.java index 047b2fcf..fdc66473 100644 --- a/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuFinder.java +++ b/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuFinder.java @@ -14,7 +14,7 @@ public class MenuFinder { private final MenuRepository menuRepository; - public List findAllByStore(Store store) { + public List findAllByStore(final Store store) { return menuRepository.findAllByStore(store); } } diff --git a/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuUpdater.java b/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuUpdater.java new file mode 100644 index 00000000..4f509e34 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/menu/service/MenuUpdater.java @@ -0,0 +1,19 @@ +package org.hankki.hankkiserver.api.menu.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.domain.menu.model.Menu; +import org.hankki.hankkiserver.domain.menu.repository.MenuRepository; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class MenuUpdater { + + private final MenuRepository menuRepository; + + public void saveAll(final List menus) { + menuRepository.saveAll(menus); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/report/service/ReportUpdater.java b/src/main/java/org/hankki/hankkiserver/api/report/service/ReportUpdater.java new file mode 100644 index 00000000..62de1995 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/report/service/ReportUpdater.java @@ -0,0 +1,17 @@ +package org.hankki.hankkiserver.api.report.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.domain.report.model.Report; +import org.hankki.hankkiserver.domain.report.repository.ReportRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class ReportUpdater { + + private final ReportRepository repository; + + public void save(final Report report) { + repository.save(report); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java b/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java index b216bc6a..96051604 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/controller/StoreController.java @@ -1,23 +1,39 @@ package org.hankki.hankkiserver.api.store.controller; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.hankki.hankkiserver.api.dto.HankkiResponse; import org.hankki.hankkiserver.api.store.controller.request.StoreDuplicateValidationRequest; +import org.hankki.hankkiserver.api.store.controller.request.StorePostRequest; +import org.hankki.hankkiserver.api.store.service.StoreCommandService; +import org.hankki.hankkiserver.api.store.service.StoreQueryService; +import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; +import org.hankki.hankkiserver.api.store.service.response.*; +import org.hankki.hankkiserver.common.code.CommonSuccessCode; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.hankki.hankkiserver.auth.UserId; import org.hankki.hankkiserver.api.store.service.HeartCommandService; import org.hankki.hankkiserver.api.store.service.StoreQueryService; -import org.hankki.hankkiserver.api.store.service.command.StoreDeleteCommand; import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; import org.hankki.hankkiserver.api.store.service.command.StoreValidationCommand; import org.hankki.hankkiserver.api.store.service.response.*; import org.hankki.hankkiserver.auth.UserId; import org.hankki.hankkiserver.common.code.CommonSuccessCode; +import org.hankki.hankkiserver.api.store.service.command.HeartDeleteCommand; +import org.hankki.hankkiserver.api.store.service.command.HeartPostCommand; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.*; +import org.springframework.web.multipart.MultipartFile; @RestController @RequiredArgsConstructor @RequestMapping("/api/v1") public class StoreController { + private final StoreCommandService storeCommandService; private final StoreQueryService storeQueryService; private final HeartCommandService heartCommandService; @@ -49,12 +65,12 @@ public HankkiResponse getPrices() { @PostMapping("/stores/{id}/hearts") public HankkiResponse createHeartStore(@UserId final Long userId, @PathVariable final Long id) { - return HankkiResponse.success(CommonSuccessCode.CREATED, heartCommandService.createHeart(StorePostCommand.of(userId, id))); + return HankkiResponse.success(CommonSuccessCode.CREATED, heartCommandService.createHeart(HeartPostCommand.of(userId, id))); } @DeleteMapping("/stores/{id}/hearts") public HankkiResponse deleteHeartStore(@UserId final Long userId, @PathVariable final Long id) { - return HankkiResponse.success(CommonSuccessCode.OK, heartCommandService.deleteHeart(StoreDeleteCommand.of(userId, id))); + return HankkiResponse.success(CommonSuccessCode.OK, heartCommandService.deleteHeart(HeartDeleteCommand.of(userId, id))); } @PostMapping("/stores/validate") @@ -62,4 +78,10 @@ public HankkiResponse validateDuplicatedStore(@RequestBody final StoreDupl storeQueryService.validateDuplicatedStore(StoreValidationCommand.of(request)); return HankkiResponse.success(CommonSuccessCode.OK); } + @PostMapping("/stores") + public HankkiResponse createStore(@RequestPart(required = false) final MultipartFile image, + @Valid @RequestPart final StorePostRequest request, + @UserId final Long userId) { + return HankkiResponse.success(CommonSuccessCode.CREATED, storeCommandService.createStore(StorePostCommand.of(image, request, userId))); + } } diff --git a/src/main/java/org/hankki/hankkiserver/api/store/controller/request/MenuPostRequest.java b/src/main/java/org/hankki/hankkiserver/api/store/controller/request/MenuPostRequest.java new file mode 100644 index 00000000..db670842 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/controller/request/MenuPostRequest.java @@ -0,0 +1,13 @@ +package org.hankki.hankkiserver.api.store.controller.request; + +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Size; + +public record MenuPostRequest( + @NotBlank @Size(max = 30) + String name, + @Max(8000) + int price +) { +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/controller/request/StorePostRequest.java b/src/main/java/org/hankki/hankkiserver/api/store/controller/request/StorePostRequest.java new file mode 100644 index 00000000..bfee6461 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/controller/request/StorePostRequest.java @@ -0,0 +1,19 @@ +package org.hankki.hankkiserver.api.store.controller.request; + +import jakarta.validation.Valid; +import jakarta.validation.constraints.Size; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; + +import java.util.List; + +public record StorePostRequest( + String name, + StoreCategory category, + String address, + double latitude, + double longitude, + long universityId, + @Size(min = 1) @Valid + List menus + ) { +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java b/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java index fc051270..38bbc20d 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/HeartCommandService.java @@ -2,8 +2,8 @@ import lombok.RequiredArgsConstructor; import org.hankki.hankkiserver.api.auth.service.UserFinder; -import org.hankki.hankkiserver.api.store.service.command.StoreDeleteCommand; -import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; +import org.hankki.hankkiserver.api.store.service.command.HeartDeleteCommand; +import org.hankki.hankkiserver.api.store.service.command.HeartPostCommand; import org.hankki.hankkiserver.api.store.service.response.HeartCreateResponse; import org.hankki.hankkiserver.api.store.service.response.HeartDeleteResponse; import org.hankki.hankkiserver.common.code.HeartErrorCode; @@ -25,18 +25,18 @@ public class HeartCommandService { private final UserFinder userFinder; private final StoreFinder storeFinder; - public HeartCreateResponse createHeart(final StorePostCommand storePostCommand) { - User user = userFinder.getUserReference(storePostCommand.userId()); - Store store = storeFinder.getStoreReference(storePostCommand.storeId()); + public HeartCreateResponse createHeart(final HeartPostCommand heartPostCommand) { + User user = userFinder.getUserReference(heartPostCommand.userId()); + Store store = storeFinder.getStoreReference(heartPostCommand.storeId()); validateStoreHeartCreation(user, store); saveStoreHeart(user, store); increaseStoreHeartCount(store); return HeartCreateResponse.of(store); } - public HeartDeleteResponse deleteHeart(final StoreDeleteCommand storeDeleteCommand) { - User user = userFinder.getUserReference(storeDeleteCommand.userId()); - Store store = storeFinder.getStoreReference(storeDeleteCommand.storeId()); + public HeartDeleteResponse deleteHeart(final HeartDeleteCommand heartDeleteCommand) { + User user = userFinder.getUserReference(heartDeleteCommand.userId()); + Store store = storeFinder.getStoreReference(heartDeleteCommand.storeId()); validateStoreHeartRemoval(user, store); heartDeleter.deleteHeart(user,store); decreaseStoreHeartCount(store); diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreCommandService.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreCommandService.java new file mode 100644 index 00000000..a4055d6f --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreCommandService.java @@ -0,0 +1,90 @@ +package org.hankki.hankkiserver.api.store.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.api.auth.service.UserFinder; +import org.hankki.hankkiserver.api.menu.service.MenuUpdater; +import org.hankki.hankkiserver.api.report.service.ReportUpdater; +import org.hankki.hankkiserver.api.store.service.command.StorePostCommand; +import org.hankki.hankkiserver.api.store.service.response.StorePostResponse; +import org.hankki.hankkiserver.api.university.service.UniversityFinder; +import org.hankki.hankkiserver.api.universitystore.service.UniversityStoreUpdater; +import org.hankki.hankkiserver.common.code.StoreErrorCode; +import org.hankki.hankkiserver.common.exception.BadRequestException; +import org.hankki.hankkiserver.domain.menu.model.Menu; +import org.hankki.hankkiserver.domain.report.model.Report; +import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.model.StoreImage; +import org.hankki.hankkiserver.domain.university.model.University; +import org.hankki.hankkiserver.domain.universitystore.model.UniversityStore; +import org.hankki.hankkiserver.external.s3.S3Service; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.IOException; +import java.util.List; + +@Service +@RequiredArgsConstructor +public class StoreCommandService { + + @Value("${store.default-image}") + private String DEFAULT_IMAGE_URL; + + private static final String STORE_IMAGE_DIRECTORY = "store/"; + + private final StoreUpdater storeUpdater; + private final MenuUpdater menuUpdater; + private final StoreImageUpdater storeImageUpdater; + private final S3Service s3Service; + private final UniversityStoreUpdater universityStoreUpdater; + private final UniversityFinder universityFinder; + private final ReportUpdater reportUpdater; + private final UserFinder userFinder; + private final StoreFinder storeFinder; + + @Transactional(rollbackFor = Exception.class) + public StorePostResponse createStore(final StorePostCommand command) { + if (storeExists(command.latitude(), command.longitude())) { + throw new BadRequestException(StoreErrorCode.BAD_STORE_INFO); + } + + Store store = storeUpdater.save(command.toEntity()); + saveImages(command, store); + menuUpdater.saveAll(getMenus(command, store)); + + University university = universityFinder.findById(command.universityId()); + universityStoreUpdater.save(UniversityStore.create(store, university)); + reportUpdater.save(Report.create(userFinder.getUser(command.userId()), store, university)); + + return StorePostResponse.of(store); + } + + private boolean storeExists(final double latitude, final double longitude) { + return storeFinder.existsByLatitudeAndLongitude(latitude, longitude); + } + + private void saveImages(final StorePostCommand command, final Store store) { + if (isNullOrEmptyImage(command)) { + storeImageUpdater.saveImage(StoreImage.createImage(store, DEFAULT_IMAGE_URL)); + } + else { + try { + String imageUrl = s3Service.uploadImage(STORE_IMAGE_DIRECTORY, command.image()); + storeImageUpdater.saveImage(StoreImage.createImage(store, imageUrl)); + } catch (IOException e) { + storeImageUpdater.saveImage(StoreImage.createImage(store, DEFAULT_IMAGE_URL)); + } + } + } + + private boolean isNullOrEmptyImage(final StorePostCommand command) { + return command.image() == null || command.image().isEmpty(); + } + + private List getMenus(final StorePostCommand command, final Store store) { + return command.menus().stream()//메뉴를 엔티티로 저장한다. + .map(menuPostRequest -> Menu.create(store, menuPostRequest.name(), menuPostRequest.price())) + .toList(); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java index bc38aca8..be94b471 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreFinder.java @@ -33,4 +33,7 @@ protected Store findByIdWithHeartAndIsDeletedFalse(final Long id) { protected Optional findByLatitudeAndLongitude(final double latitude, final double longitude) { return storeRepository.findByPoint_LatitudeAndPoint_Longitude(latitude, longitude); } + protected boolean existsByLatitudeAndLongitude(final double latitude, final double longitude) { + return storeRepository.existsByPoint_LatitudeAndPoint_Longitude(latitude, longitude); + } } diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreImageUpdater.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreImageUpdater.java new file mode 100644 index 00000000..f9df55f1 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreImageUpdater.java @@ -0,0 +1,17 @@ +package org.hankki.hankkiserver.api.store.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.domain.store.model.StoreImage; +import org.hankki.hankkiserver.domain.store.repository.StoreImageRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StoreImageUpdater { + + private final StoreImageRepository storeImageRepository; + + public void saveImage(final StoreImage image) { + storeImageRepository.save(image); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/StoreUpdater.java b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreUpdater.java new file mode 100644 index 00000000..93141e50 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/StoreUpdater.java @@ -0,0 +1,17 @@ +package org.hankki.hankkiserver.api.store.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.repository.StoreRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class StoreUpdater { + + private final StoreRepository storeRepository; + + public Store save(final Store store) { + return storeRepository.save(store); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartDeleteCommand.java b/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartDeleteCommand.java new file mode 100644 index 00000000..5850af17 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartDeleteCommand.java @@ -0,0 +1,11 @@ +package org.hankki.hankkiserver.api.store.service.command; + +public record HeartDeleteCommand( + Long userId, + Long storeId +) { + public static HeartDeleteCommand of(final Long userId, final Long storeId) { + return new HeartDeleteCommand(userId, storeId); + } +} + diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartPostCommand.java b/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartPostCommand.java new file mode 100644 index 00000000..7b07c445 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/command/HeartPostCommand.java @@ -0,0 +1,10 @@ +package org.hankki.hankkiserver.api.store.service.command; + +public record HeartPostCommand( + Long userId, + Long storeId +) { + public static HeartPostCommand of(final Long userId, final Long storeId) { + return new HeartPostCommand(userId, storeId); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/command/StoreDeleteCommand.java b/src/main/java/org/hankki/hankkiserver/api/store/service/command/StoreDeleteCommand.java deleted file mode 100644 index b98b8d7e..00000000 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/command/StoreDeleteCommand.java +++ /dev/null @@ -1,11 +0,0 @@ -package org.hankki.hankkiserver.api.store.service.command; - -public record StoreDeleteCommand( - Long userId, - Long storeId -) { - public static StoreDeleteCommand of(final Long userId, final Long storeId) { - return new StoreDeleteCommand(userId, storeId); - } -} - diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/command/StorePostCommand.java b/src/main/java/org/hankki/hankkiserver/api/store/service/command/StorePostCommand.java index 6eef5635..2b0a8e50 100644 --- a/src/main/java/org/hankki/hankkiserver/api/store/service/command/StorePostCommand.java +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/command/StorePostCommand.java @@ -1,10 +1,46 @@ package org.hankki.hankkiserver.api.store.service.command; -public record StorePostCommand( +import org.hankki.hankkiserver.api.store.controller.request.MenuPostRequest; +import org.hankki.hankkiserver.api.store.controller.request.StorePostRequest; +import org.hankki.hankkiserver.domain.common.Point; +import org.hankki.hankkiserver.domain.store.model.Store; +import org.hankki.hankkiserver.domain.store.model.StoreCategory; +import org.springframework.web.multipart.MultipartFile; + +import java.util.List; + +public record StorePostCommand ( + MultipartFile image, + String name, + String address, + StoreCategory category, + double latitude, + double longitude, + Long universityId, Long userId, - Long storeId + List menus ) { - public static StorePostCommand of(final Long userId, final Long storeId) { - return new StorePostCommand(userId, storeId); + public static StorePostCommand of(final MultipartFile image, final StorePostRequest request, final Long userId) { + return new StorePostCommand(image, + request.name(), + request.address(), + request.category(), + request.latitude(), + request.longitude(), + request.universityId(), + userId, + request.menus()); + } + + public Store toEntity() { + return Store.builder() + .name(name()) + .point(new Point(latitude(), longitude())) + .address(address()) + .category(category()) + .lowestPrice(menus().stream().mapToInt(menu -> menu.price()).min().orElse(0)) + .heartCount(0) + .isDeleted(false) + .build(); } } diff --git a/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePostResponse.java b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePostResponse.java new file mode 100644 index 00000000..65879359 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/store/service/response/StorePostResponse.java @@ -0,0 +1,12 @@ +package org.hankki.hankkiserver.api.store.service.response; + +import org.hankki.hankkiserver.domain.store.model.Store; + +public record StorePostResponse ( + long id, + String name +) { + public static StorePostResponse of(final Store store) { + return new StorePostResponse(store.getId(), store.getName()); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/api/university/service/UniversityFinder.java b/src/main/java/org/hankki/hankkiserver/api/university/service/UniversityFinder.java index b5b582cf..fbf08139 100644 --- a/src/main/java/org/hankki/hankkiserver/api/university/service/UniversityFinder.java +++ b/src/main/java/org/hankki/hankkiserver/api/university/service/UniversityFinder.java @@ -1,6 +1,8 @@ package org.hankki.hankkiserver.api.university.service; import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.common.code.UniversityErrorCode; +import org.hankki.hankkiserver.common.exception.NotFoundException; import org.hankki.hankkiserver.domain.university.model.University; import org.hankki.hankkiserver.domain.university.repository.UniversityRepository; import org.springframework.stereotype.Component; @@ -13,7 +15,11 @@ public class UniversityFinder { private final UniversityRepository universityRepository; - public List findAll() { + public University findById(final Long id) { + return universityRepository.findById(id) + .orElseThrow(() -> new NotFoundException(UniversityErrorCode.UNIVERSITY_NOT_FOUND)); + } + public List findAll () { return universityRepository.findAll(); } } diff --git a/src/main/java/org/hankki/hankkiserver/api/universitystore/service/UniversityStoreUpdater.java b/src/main/java/org/hankki/hankkiserver/api/universitystore/service/UniversityStoreUpdater.java new file mode 100644 index 00000000..9d1ea587 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/api/universitystore/service/UniversityStoreUpdater.java @@ -0,0 +1,17 @@ +package org.hankki.hankkiserver.api.universitystore.service; + +import lombok.RequiredArgsConstructor; +import org.hankki.hankkiserver.domain.universitystore.model.UniversityStore; +import org.hankki.hankkiserver.domain.universitystore.repository.UniversityStoreRepository; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class UniversityStoreUpdater { + + private final UniversityStoreRepository universityStoreRepository; + + public void save(UniversityStore universityStore) { + universityStoreRepository.save(universityStore); + } +} diff --git a/src/main/java/org/hankki/hankkiserver/common/code/StoreErrorCode.java b/src/main/java/org/hankki/hankkiserver/common/code/StoreErrorCode.java index 16dc03de..7c3ba6ba 100644 --- a/src/main/java/org/hankki/hankkiserver/common/code/StoreErrorCode.java +++ b/src/main/java/org/hankki/hankkiserver/common/code/StoreErrorCode.java @@ -9,7 +9,9 @@ public enum StoreErrorCode implements ErrorCode { STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "가게를 찾을 수 없습니다."), - STORE_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 등록된 가게입니다."); + STORE_ALREADY_REGISTERED(HttpStatus.CONFLICT, "이미 등록된 가게입니다."), + STORE_FILE_SIZE_EXCEEDED(HttpStatus.BAD_REQUEST, "파일 크기가 초과되었습니다."), + BAD_STORE_INFO(HttpStatus.BAD_REQUEST, "잘못된 식당 정보입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/main/java/org/hankki/hankkiserver/common/code/UniversityErrorCode.java b/src/main/java/org/hankki/hankkiserver/common/code/UniversityErrorCode.java new file mode 100644 index 00000000..cd4c3d95 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/common/code/UniversityErrorCode.java @@ -0,0 +1,16 @@ +package org.hankki.hankkiserver.common.code; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@Getter +@RequiredArgsConstructor +public enum UniversityErrorCode implements ErrorCode { + + UNIVERSITY_NOT_FOUND(HttpStatus.NOT_FOUND, "대학교를 찾을 수 없습니다."); + + private final HttpStatus httpStatus; + private final String message; + +} diff --git a/src/main/java/org/hankki/hankkiserver/domain/common/Point.java b/src/main/java/org/hankki/hankkiserver/domain/common/Point.java index 03e24069..38d3fcb0 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/common/Point.java +++ b/src/main/java/org/hankki/hankkiserver/domain/common/Point.java @@ -2,10 +2,15 @@ import jakarta.persistence.Column; import jakarta.persistence.Embeddable; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Embeddable +@AllArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Point { @Column(nullable = false) @@ -13,5 +18,4 @@ public class Point { @Column(nullable = false) private double longitude; - } diff --git a/src/main/java/org/hankki/hankkiserver/domain/menu/model/Menu.java b/src/main/java/org/hankki/hankkiserver/domain/menu/model/Menu.java index 19b5b1cf..6b53f9eb 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/menu/model/Menu.java +++ b/src/main/java/org/hankki/hankkiserver/domain/menu/model/Menu.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hankki.hankkiserver.domain.common.BaseTimeEntity; @@ -27,4 +28,18 @@ public class Menu extends BaseTimeEntity { @Column(nullable = false) private int price; + @Builder + private Menu(Store store, String name, int price) { + this.store = store; + this.name = name; + this.price = price; + } + + public static Menu create(final Store store, final String name, final int price) { + return Menu.builder() + .store(store) + .name(name) + .price(price) + .build(); + } } diff --git a/src/main/java/org/hankki/hankkiserver/domain/report/model/Report.java b/src/main/java/org/hankki/hankkiserver/domain/report/model/Report.java index ffe2e835..976fa081 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/report/model/Report.java +++ b/src/main/java/org/hankki/hankkiserver/domain/report/model/Report.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hankki.hankkiserver.domain.common.BaseCreatedAtEntity; @@ -31,4 +32,18 @@ public class Report extends BaseCreatedAtEntity { @JoinColumn(name = "university_id") private University university; + @Builder + private Report (User user, Store store, University university) { + this.user = user; + this.store = store; + this.university = university; + } + + public static Report create(final User user, final Store store, final University university) { + return Report.builder() + .user(user) + .store(store) + .university(university) + .build(); + } } diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java index 91e9da66..b4b10969 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/model/Store.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.List; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hankki.hankkiserver.domain.common.BaseTimeEntity; @@ -26,6 +27,13 @@ public class Store extends BaseTimeEntity { @OneToMany(mappedBy = "store") private List hearts = new ArrayList<>(); + @OneToMany(mappedBy = "store") + @BatchSize(size = 100) + private List images = new ArrayList<>(); + + @OneToMany(mappedBy = "store") + private List universityStores = new ArrayList<>(); + @Embedded private Point point; @@ -36,6 +44,9 @@ public class Store extends BaseTimeEntity { @Column(nullable = false) private StoreCategory category; + @Column(nullable = false) + private String address; + @Column(nullable = false) private int heartCount; @@ -45,12 +56,23 @@ public class Store extends BaseTimeEntity { @Column(nullable = false) private boolean isDeleted; - @OneToMany(mappedBy = "store") - @BatchSize(size = 100) - private List images = new ArrayList<>(); + @Builder + private Store (String name, Point point, String address, StoreCategory category, int lowestPrice, int heartCount, boolean isDeleted) { + this.name = name; + this.point = point; + this.address = address; + this.category = category; + this.lowestPrice = lowestPrice; + this.heartCount = heartCount; + this.isDeleted = isDeleted; + } - @OneToMany(mappedBy = "store") - private List universityStores = new ArrayList<>(); + public String getImage() { + if (images.isEmpty()) { + return "default.com"; + } + return images.get(0).getImageUrl(); + } public void decreaseHeartCount() { this.heartCount--; diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/model/StoreImage.java b/src/main/java/org/hankki/hankkiserver/domain/store/model/StoreImage.java index 6e8b7311..15711c9d 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/model/StoreImage.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/model/StoreImage.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hankki.hankkiserver.domain.common.BaseCreatedAtEntity; @@ -23,4 +24,16 @@ public class StoreImage extends BaseCreatedAtEntity { @Column(nullable = false) private String imageUrl; + @Builder + public StoreImage (Store store, String imageUrl) { + this.store = store; + this.imageUrl = imageUrl; + } + + public static StoreImage createImage(Store store, String imageUrl) { + return StoreImage.builder() + .store(store) + .imageUrl(imageUrl) + .build(); + } } diff --git a/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java b/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java index 349e057b..15a7d5c0 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java +++ b/src/main/java/org/hankki/hankkiserver/domain/store/repository/StoreRepository.java @@ -17,4 +17,6 @@ public interface StoreRepository extends JpaRepository { Optional findByPoint_LatitudeAndPoint_Longitude(double latitude, double longitude); Optional findByName(String name); + + boolean existsByPoint_LatitudeAndPoint_Longitude(double latitude, double longitude); } diff --git a/src/main/java/org/hankki/hankkiserver/domain/universitystore/model/UniversityStore.java b/src/main/java/org/hankki/hankkiserver/domain/universitystore/model/UniversityStore.java index be6147f2..2a645975 100644 --- a/src/main/java/org/hankki/hankkiserver/domain/universitystore/model/UniversityStore.java +++ b/src/main/java/org/hankki/hankkiserver/domain/universitystore/model/UniversityStore.java @@ -2,6 +2,7 @@ import jakarta.persistence.*; import lombok.AccessLevel; +import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; import org.hankki.hankkiserver.domain.store.model.Store; @@ -24,4 +25,17 @@ public class UniversityStore { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "store_id") private Store store; + + @Builder + private UniversityStore(University university, Store store) { + this.university = university; + this.store = store; + } + + public static UniversityStore create(final Store store, final University university) { + return UniversityStore.builder() + .store(store) + .university(university) + .build(); + } } diff --git a/src/main/java/org/hankki/hankkiserver/external/s3/AwsConfig.java b/src/main/java/org/hankki/hankkiserver/external/s3/AwsConfig.java new file mode 100644 index 00000000..1a0a9ff3 --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/external/s3/AwsConfig.java @@ -0,0 +1,47 @@ +package org.hankki.hankkiserver.external.s3; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import software.amazon.awssdk.auth.credentials.SystemPropertyCredentialsProvider; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.s3.S3Client; + +@Configuration +public class AwsConfig { + + private static final String AWS_ACCESS_KEY_ID = "aws.accessKeyId"; + private static final String AWS_SECRET_ACCESS_KEY = "aws.secretAccessKey"; + + private final String accessKey; + private final String secretKey; + private final String regionString; + + public AwsConfig(@Value("${aws-property.access-key}") final String accessKey, + @Value("${aws-property.secret-key}") final String secretKey, + @Value("${aws-property.aws-region}") final String regionString) { + this.accessKey = accessKey; + this.secretKey = secretKey; + this.regionString = regionString; + } + + @Bean + public SystemPropertyCredentialsProvider systemPropertyCredentialsProvider() { + System.setProperty(AWS_ACCESS_KEY_ID, accessKey); + System.setProperty(AWS_SECRET_ACCESS_KEY, secretKey); + return SystemPropertyCredentialsProvider.create(); + } + + @Bean + public Region getRegion() { + return Region.of(regionString); + } + + @Bean + public S3Client getS3Client() { + return S3Client.builder() + .region(getRegion()) + .credentialsProvider(systemPropertyCredentialsProvider()) + .build(); + } +} \ No newline at end of file diff --git a/src/main/java/org/hankki/hankkiserver/external/s3/S3Service.java b/src/main/java/org/hankki/hankkiserver/external/s3/S3Service.java new file mode 100644 index 00000000..657215bc --- /dev/null +++ b/src/main/java/org/hankki/hankkiserver/external/s3/S3Service.java @@ -0,0 +1,82 @@ +package org.hankki.hankkiserver.external.s3; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.web.multipart.MultipartFile; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.UUID; + +@Component +public class S3Service { + + @Value("${aws-property.s3-endpoint}") + private String bucketUrl; + private final String bucketName; + private final AwsConfig awsConfig; + private static final List IMAGE_EXTENSIONS = Arrays.asList("image/jpeg", "image/png", "image/jpg", "image/webp"); + + + public S3Service(@Value("${aws-property.s3-bucket-name}") final String bucketName, AwsConfig awsConfig) { + this.bucketName = bucketName; + this.awsConfig = awsConfig; + } + + + public String uploadImage(String directoryPath, MultipartFile image) throws IOException { + final String key = directoryPath + generateImageFileName(); + final S3Client s3Client = awsConfig.getS3Client(); + + validateExtension(image); + validateFileSize(image); + + PutObjectRequest request = PutObjectRequest.builder() + .bucket(bucketName) + .key(key) + .contentType(image.getContentType()) + .contentDisposition("inline") + .build(); + + RequestBody requestBody = RequestBody.fromBytes(image.getBytes()); + s3Client.putObject(request, requestBody); + return bucketUrl + key; + } + + public void deleteImage(String key) throws IOException { + final S3Client s3Client = awsConfig.getS3Client(); + + s3Client.deleteObject((DeleteObjectRequest.Builder builder) -> + builder.bucket(bucketName) + .key(key) + .build() + ); + } + + + private String generateImageFileName() { + return UUID.randomUUID() + ".jpg"; + } + + + private void validateExtension(MultipartFile image) { + String contentType = image.getContentType(); + if (!IMAGE_EXTENSIONS.contains(contentType)) { + throw new RuntimeException("이미지 확장자는 jpg, png, webp만 가능합니다."); + } + } + + private static final Long MAX_FILE_SIZE = 5 * 1024 * 1024L; + + private void validateFileSize(MultipartFile image) { + if (image.getSize() > MAX_FILE_SIZE) { + throw new RuntimeException("이미지 사이즈는 5MB를 넘을 수 없습니다."); + } + } + +}