diff --git a/src/main/java/com/pickyfy/pickyfy/PickyfyApplication.java b/src/main/java/com/pickyfy/pickyfy/PickyfyApplication.java index d6e6879..5edf47b 100644 --- a/src/main/java/com/pickyfy/pickyfy/PickyfyApplication.java +++ b/src/main/java/com/pickyfy/pickyfy/PickyfyApplication.java @@ -4,6 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +// TODO: 비동기 처리를 위한 @EnableAsync 활성화 필요 @SpringBootApplication @EnableJpaAuditing public class PickyfyApplication { diff --git a/src/main/java/com/pickyfy/pickyfy/common/AllFieldsNotNull.java b/src/main/java/com/pickyfy/pickyfy/common/AllFieldsNotNull.java new file mode 100644 index 0000000..07cab6f --- /dev/null +++ b/src/main/java/com/pickyfy/pickyfy/common/AllFieldsNotNull.java @@ -0,0 +1,21 @@ +package com.pickyfy.pickyfy.common; + +import com.pickyfy.pickyfy.common.util.AllFieldsNotNullValidator; +import jakarta.validation.Constraint; +import jakarta.validation.Payload; +import jakarta.validation.constraints.NotNull; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = {AllFieldsNotNullValidator.class}) +@NotNull +public @interface AllFieldsNotNull { + String message() default "모든 필드는 not Null"; + Class[] groups() default {}; + Class[] payload() default {}; +} diff --git a/src/main/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidator.java b/src/main/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidator.java new file mode 100644 index 0000000..33a9ddc --- /dev/null +++ b/src/main/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidator.java @@ -0,0 +1,30 @@ +package com.pickyfy.pickyfy.common.util; + +import com.pickyfy.pickyfy.common.AllFieldsNotNull; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; + +import java.lang.reflect.Field; + +public class AllFieldsNotNullValidator implements ConstraintValidator { + + @Override + public boolean isValid(Object object, ConstraintValidatorContext context){ + if (object == null) { + return false; // 클래스 자체가 null이면 false 반환 + } + + // 클래스에 선언된 모든 필드 가져와서 순회하고 null 검사 + for (Field field : object.getClass().getDeclaredFields()) { + field.setAccessible(true); // private, protected 접근자 붙었더라도 필드에 강제 접근 가능 + try { + if (field.get(object) == null) { + return false; + } + } catch (IllegalAccessException e) { + throw new RuntimeException("필드 접근 실패", e); + } + } + return true; // 모든 필드가 null이 아닐 때만 true 반환 + } +} diff --git a/src/main/java/com/pickyfy/pickyfy/repository/UserSavedPlaceRepository.java b/src/main/java/com/pickyfy/pickyfy/repository/UserSavedPlaceRepository.java index 8e1ea2e..9c22eb1 100644 --- a/src/main/java/com/pickyfy/pickyfy/repository/UserSavedPlaceRepository.java +++ b/src/main/java/com/pickyfy/pickyfy/repository/UserSavedPlaceRepository.java @@ -1,17 +1,12 @@ package com.pickyfy.pickyfy.repository; -import com.pickyfy.pickyfy.domain.Place; import com.pickyfy.pickyfy.domain.UserSavedPlace; import org.springframework.data.jpa.repository.JpaRepository; - import java.util.List; import java.util.Optional; - public interface UserSavedPlaceRepository extends JpaRepository { List findAllByUserId(Long id); - UserSavedPlace findByUserId(Long userId); - Optional findByUserIdAndPlaceId(Long userId, Long placeId); } diff --git a/src/main/java/com/pickyfy/pickyfy/service/CategoryService.java b/src/main/java/com/pickyfy/pickyfy/service/CategoryService.java index 3995efb..c496a1c 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/CategoryService.java +++ b/src/main/java/com/pickyfy/pickyfy/service/CategoryService.java @@ -1,14 +1,13 @@ package com.pickyfy.pickyfy.service; -import com.pickyfy.pickyfy.web.dto.request.CategoryCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.CategoryUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.CategoryTypeRequest; import com.pickyfy.pickyfy.web.dto.response.CategoryResponse; import java.util.List; public interface CategoryService { - Long createCategory(CategoryCreateRequest categoryCreateRequest); + Long createCategory(CategoryTypeRequest categoryCreateRequest); CategoryResponse getCategory(Long id); List getAllCategories(); - void updateCategory(Long id, CategoryUpdateRequest request); + void updateCategory(Long id, CategoryTypeRequest request); void deleteCategory(Long id); } diff --git a/src/main/java/com/pickyfy/pickyfy/service/CategoryServiceImpl.java b/src/main/java/com/pickyfy/pickyfy/service/CategoryServiceImpl.java index e6fb1eb..25db451 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/CategoryServiceImpl.java +++ b/src/main/java/com/pickyfy/pickyfy/service/CategoryServiceImpl.java @@ -5,8 +5,7 @@ import com.pickyfy.pickyfy.domain.CategoryType; import com.pickyfy.pickyfy.exception.DuplicateResourceException; import com.pickyfy.pickyfy.repository.CategoryRepository; -import com.pickyfy.pickyfy.web.dto.request.CategoryCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.CategoryUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.CategoryTypeRequest; import com.pickyfy.pickyfy.web.dto.response.CategoryResponse; import jakarta.persistence.EntityNotFoundException; import java.util.List; @@ -24,7 +23,7 @@ public class CategoryServiceImpl implements CategoryService { @Override @Transactional - public Long createCategory(CategoryCreateRequest categoryCreateRequest) { + public Long createCategory(CategoryTypeRequest categoryCreateRequest) { validateDuplicateType(categoryCreateRequest.categoryType()); Category category = Category.builder() @@ -49,13 +48,11 @@ public List getAllCategories() { @Override @Transactional - public void updateCategory(Long id, CategoryUpdateRequest request) { + public void updateCategory(Long id, CategoryTypeRequest request) { Category category = findCategoryById(id); - if (category.getType() != request.categoryType()) { validateDuplicateType(request.categoryType()); } - category.update(request.categoryType()); } diff --git a/src/main/java/com/pickyfy/pickyfy/service/EmailService.java b/src/main/java/com/pickyfy/pickyfy/service/EmailService.java index a8ba7f3..6a7a1ba 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/EmailService.java +++ b/src/main/java/com/pickyfy/pickyfy/service/EmailService.java @@ -6,8 +6,6 @@ import com.pickyfy.pickyfy.web.dto.response.EmailVerificationVerifyResponse; public interface EmailService { - EmailVerificationSendResponse sendAuthCode(EmailVerificationSendRequest request); - EmailVerificationVerifyResponse verifyAuthCode(EmailVerificationVerifyRequest emailVerificationVerifyRequest); } \ No newline at end of file diff --git a/src/main/java/com/pickyfy/pickyfy/service/MagazineService.java b/src/main/java/com/pickyfy/pickyfy/service/MagazineService.java index 30cf5df..594335b 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/MagazineService.java +++ b/src/main/java/com/pickyfy/pickyfy/service/MagazineService.java @@ -1,14 +1,13 @@ package com.pickyfy.pickyfy.service; -import com.pickyfy.pickyfy.web.dto.request.MagazineCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.MagazineUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.MagazineRequest; import com.pickyfy.pickyfy.web.dto.response.MagazineResponse; import java.util.List; public interface MagazineService { - Long createMagazine(MagazineCreateRequest request); + Long createMagazine(MagazineRequest request); MagazineResponse getMagazine(Long id); List getAllMagazines(); - void updateMagazine(Long id, MagazineUpdateRequest request); + void updateMagazine(Long id, MagazineRequest request); void deleteMagazine(Long id); } diff --git a/src/main/java/com/pickyfy/pickyfy/service/MagazineServiceImpl.java b/src/main/java/com/pickyfy/pickyfy/service/MagazineServiceImpl.java index 3356a77..086ce00 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/MagazineServiceImpl.java +++ b/src/main/java/com/pickyfy/pickyfy/service/MagazineServiceImpl.java @@ -4,8 +4,7 @@ import com.pickyfy.pickyfy.domain.Magazine; import com.pickyfy.pickyfy.exception.DuplicateResourceException; import com.pickyfy.pickyfy.repository.MagazineRepository; -import com.pickyfy.pickyfy.web.dto.request.MagazineCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.MagazineUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.MagazineRequest; import com.pickyfy.pickyfy.web.dto.response.MagazineResponse; import jakarta.persistence.EntityNotFoundException; import java.util.List; @@ -24,7 +23,7 @@ public class MagazineServiceImpl implements MagazineService { @Override @Transactional - public Long createMagazine(MagazineCreateRequest request) { + public Long createMagazine(MagazineRequest request) { validateDuplicateTitle(request.title()); Magazine magazine = Magazine.builder() .title(request.title()) @@ -49,7 +48,7 @@ public List getAllMagazines() { @Override @Transactional - public void updateMagazine(Long id, MagazineUpdateRequest request) { + public void updateMagazine(Long id, MagazineRequest request) { Magazine magazine = findMagazineById(id); if (request.iconFile() == null){ magazine.update(request.title()); diff --git a/src/main/java/com/pickyfy/pickyfy/service/PlaceServiceImpl.java b/src/main/java/com/pickyfy/pickyfy/service/PlaceServiceImpl.java index 54edd74..d3170f8 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/PlaceServiceImpl.java +++ b/src/main/java/com/pickyfy/pickyfy/service/PlaceServiceImpl.java @@ -3,23 +3,23 @@ import com.pickyfy.pickyfy.exception.ExceptionHandler; import com.pickyfy.pickyfy.web.apiResponse.error.ErrorStatus; import com.pickyfy.pickyfy.domain.*; +import com.pickyfy.pickyfy.web.dto.MagazineInfo; import com.pickyfy.pickyfy.web.dto.NearbyPlaceSearchCondition; +import com.pickyfy.pickyfy.web.dto.PlaceSearchResponseParams; import com.pickyfy.pickyfy.web.dto.request.NearbyPlaceSearchRequest; import com.pickyfy.pickyfy.web.dto.request.PlaceCreateRequest; import com.pickyfy.pickyfy.web.dto.response.PlaceSearchResponse; import com.pickyfy.pickyfy.repository.*; import jakarta.persistence.EntityExistsException; import jakarta.persistence.EntityNotFoundException; -import java.math.BigDecimal; -import java.util.Collections; + +import java.util.*; + import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; import java.util.stream.Collectors; @Service @@ -34,26 +34,17 @@ public class PlaceServiceImpl implements PlaceService { private final CategoryRepository categoryRepository; private final MagazineRepository magazineRepository; private final PlaceMagazineRepository placeMagazineRepository; - private final S3Service s3Service; private final PlaceCategoryRepository placeCategoryRepository; + private final S3Service s3Service; + /** * 특정 유저가 저장한 Place 전체 조회 - * @param - * @return - */ - /** - * 특정 유저가 저장한 Place 전체 조회 - * - * @param email - * @return List */ @Override public List getUserSavePlace(String email) { // 유저 조회 - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.USER_NOT_FOUND.getMessage())); - + User user = findUserByEmail(email); List allUserSavedPlaceList = userSavedPlaceRepository.findAllByUserId(user.getId()); if (allUserSavedPlaceList.isEmpty()) { @@ -66,106 +57,54 @@ public List getUserSavePlace(String email) { return allPlaceList.stream() .map(place -> { + Long placeId = place.getId(); + // 유저가 저장한 Place의 카테고리 조회 - Optional savedPlaceCategory = Optional.ofNullable(placeCategoryRepository.findByPlaceId(place.getId())); - Optional savedCategory = savedPlaceCategory.flatMap(pc -> categoryRepository.findById(pc.getCategory().getId())); + Category savedCategory = Optional.ofNullable(placeCategoryRepository.findByPlaceId(placeId)) + .map(PlaceCategory::getCategory) + .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.CATEGORY_NOT_FOUND.getMessage())); // 유저가 저장한 Place의 매거진 조회 - Optional savedPlaceMagazine = Optional.ofNullable(placeMagazineRepository.findByPlaceId(place.getId())); - Optional savedMagazine = savedPlaceMagazine.flatMap(pm -> magazineRepository.findById(pm.getMagazine().getId())); + Magazine savedMagazine = Optional.ofNullable(placeMagazineRepository.findByPlaceId(placeId)) + .map(PlaceMagazine::getMagazine) + .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.CATEGORY_NOT_FOUND.getMessage())); // 유저가 저장한 Place의 이미지 조회 - List placeImagesUrl = placeImageRepository.findAllByPlaceId(place.getId()); - - return PlaceSearchResponse.builder() - .placeId(place.getId()) - .name(place.getName()) - .shortDescription(place.getShortDescription()) - .latitude(place.getLatitude()) - .longitude(place.getLongitude()) - .createdAt(place.getCreatedAt()) - .updatedAt(place.getUpdatedAt()) - .placeImageUrl(placeImagesUrl) - .categoryName(savedCategory.map(Category::getName).orElse(null)) - .magazineTitle(savedMagazine.map(Magazine::getTitle).orElse(null)) - .instagramLink(place.getInstagramLink()) - .naverLink(place.getNaverplaceLink()) - .iconUrl(savedMagazine.map(Magazine::getIconUrl).orElse(null)) - .build(); + return PlaceSearchResponse.from(createPlaceSearchResponseParams(place, savedCategory, savedMagazine)); }) .collect(Collectors.toList()); } - /** * 특정 플레이스 조회 - * @param placeId - * @return */ - @Override public PlaceSearchResponse getPlace(Long placeId) { - Place place = findPlaceById(placeId); - List searchPlaceImageUrl = placeImageRepository.findAllByPlaceId(placeId); - - Category category = findCategoryByPlaceId(placeId); - Magazine magazine = findMagazineByPlaceId(placeId); - - List placeImagesIdList = place.getPlaceImages().stream() - .map(PlaceImage::getId) - .toList(); - - return PlaceSearchResponse.builder() - .placeId(placeId) - .placeImageUrl(searchPlaceImageUrl) - .shortDescription(place.getShortDescription()) - .name(place.getName()) - .createdAt(place.getCreatedAt()) - .updatedAt(place.getUpdatedAt()) - .longitude(place.getLongitude()) - .latitude(place.getLatitude()) - .categoryName(category.getName()) - .magazineTitle(magazine.getTitle()) - .instagramLink(place.getInstagramLink()) - .naverLink(place.getNaverplaceLink()) - .placeImageId(placeImagesIdList) - .iconUrl(magazine.getIconUrl()) - .build(); + return PlaceSearchResponse.from(createPlaceSearchResponseParams( + findPlaceById(placeId), + findCategoryByPlaceId(placeId), + findMagazineByPlaceId(placeId))); } - /** * 유저 Place 저장 및 저장취소 (toggle) - * @param - * @param placeId - * @return */ @Transactional public boolean togglePlaceUser(String email, Long placeId) { - - Place place = placeRepository.findById(placeId) - .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.PLACE_NOT_FOUND.getMessage())); - - User user = userRepository.findByEmail(email) - .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.USER_NOT_FOUND.getMessage())); + Place place = findPlaceById(placeId); + User user = findUserByEmail(email); Optional userSavedPlace = userSavedPlaceRepository.findByUserIdAndPlaceId(user.getId(), place.getId()); - if (userSavedPlace.isPresent()) { - // 저장 취소 - userSavedPlaceRepository.delete(userSavedPlace.get()); + return userSavedPlace.map(savedPlace -> { + userSavedPlaceRepository.delete(savedPlace); return false; - } else { - // 저장 - UserSavedPlace newUserSavedPlace = new UserSavedPlace(user, place); - userSavedPlaceRepository.save(newUserSavedPlace); + }).orElseGet(() -> { + userSavedPlaceRepository.save(new UserSavedPlace(user, place)); return true; - } + }); } - - - @Override public List searchNearbyPlaces(NearbyPlaceSearchRequest request) { NearbyPlaceSearchCondition condition = NearbyPlaceSearchCondition.from(request); @@ -174,11 +113,11 @@ public List searchNearbyPlaces(NearbyPlaceSearchRequest request) { @Transactional public Long createPlace(PlaceCreateRequest request, List imageList) { - if (placeRepository.existsPlaceByName(request.name())) { throw new EntityExistsException(ErrorStatus.PLACE_NAME_DUPLICATED.getMessage()); } + // TODO: N+1 문제 해결 Category category = categoryRepository.findById(request.categoryId()).orElseThrow(() -> new EntityNotFoundException(ErrorStatus.CATEGORY_NOT_FOUND.getMessage())); @@ -187,18 +126,12 @@ public Long createPlace(PlaceCreateRequest request, List imageLis // 1. Place 먼저 저장 Place newPlace = buildPlace(request); - newPlace = placeRepository.save(newPlace); - - PlaceCategory newPlaceCategory = buildPlaceCategory(category, newPlace); - placeCategoryRepository.save(newPlaceCategory); - - PlaceMagazine newPlaceMagazine = buildPlaceMagazine(magazine, newPlace); - placeMagazineRepository.save(newPlaceMagazine); + placeRepository.save(newPlace); + placeCategoryRepository.save(buildPlaceCategory(category, newPlace)); + placeMagazineRepository.save(buildPlaceMagazine(magazine, newPlace)); List placeImages = new ArrayList<>(); - int maxImages = Math.min(imageList.size(), 5); - - for (int i = 0; i < maxImages; i++) { + for (int i = 0; i < Math.min(imageList.size(), 5); i++) { String imageUrl = s3Service.upload(imageList.get(i)); PlaceImage newPlaceImage = PlaceImage.builder() .place(newPlace) @@ -209,8 +142,6 @@ public Long createPlace(PlaceCreateRequest request, List imageLis } newPlace.getPlaceImages().addAll(placeImages); - placeRepository.save(newPlace); - return newPlace.getId(); } @@ -218,6 +149,7 @@ public Long createPlace(PlaceCreateRequest request, List imageLis @Transactional public Long updatePlace(Long placeId, PlaceCreateRequest request, List imageList) { Place place = findPlaceById(placeId); + place.updatePlace(request); updatePlaceCategory(request.categoryId(), placeId, place); updatePlaceMagazine(request.magazineId(), placeId, place); @@ -239,46 +171,28 @@ public void deletePlace(Long placeId) { @Transactional(readOnly = true) public List getAllPlaces() { List allPlaceList = placeRepository.findAll(); - return allPlaceList.stream() .map(place -> { - - PlaceCategory placeCategory = placeCategoryRepository.findByPlaceId(place.getId()); - String categoryName = (placeCategory != null) ? - placeCategory.getCategory().getName() : "카테고리 없음"; - - PlaceMagazine placeMagazine = placeMagazineRepository.findByPlaceId(place.getId()); - String magazineTitle = (placeMagazine != null) ? - placeMagazine.getMagazine().getTitle() : "매거진 없음"; - - List placeImages = place.getPlaceImages().stream() - .map(PlaceImage::getUrl) - .collect(Collectors.toList()); - - List placeImagesIdList = place.getPlaceImages().stream() - .map(PlaceImage::getId) - .toList(); - - return PlaceSearchResponse.builder() - .placeId(place.getId()) - .name(place.getName()) - .shortDescription(place.getShortDescription()) - .latitude(place.getLatitude()) - .longitude(place.getLongitude()) - .createdAt(place.getCreatedAt()) - .updatedAt(place.getUpdatedAt()) - .placeImageUrl(placeImages) - .categoryName(categoryName) - .magazineTitle(magazineTitle) - .instagramLink(place.getInstagramLink()) - .naverLink(place.getNaverplaceLink()) - .placeImageId(placeImagesIdList) - .iconUrl(placeMagazine.getMagazine().getIconUrl()) - .build(); + Long placeId = place.getId(); + return PlaceSearchResponse.from( + createPlaceSearchResponseParams( + place, + placeCategoryRepository.findByPlaceId(placeId).getCategory(), + placeMagazineRepository.findByPlaceId(placeId).getMagazine()) + ); }) .collect(Collectors.toList()); } + private PlaceSearchResponseParams createPlaceSearchResponseParams(Place place, Category category, Magazine magazine){ + return new PlaceSearchResponseParams( + place, + placeImageRepository.findAllByPlaceId(place.getId()), + category.getName(), + new MagazineInfo(magazine.getTitle(), magazine.getIconUrl()) + ); + } + private Category findCategoryByPlaceId(Long placeId){ PlaceCategory savedPlaceCategory = placeCategoryRepository.findByPlaceId(placeId); if (savedPlaceCategory == null || savedPlaceCategory.getCategory() == null) { @@ -298,7 +212,6 @@ private Magazine findMagazineByPlaceId(Long placeId){ .orElseThrow(() -> new EntityNotFoundException(ErrorStatus.MAGAZINE_NOT_FOUND.getMessage())); } - private User findUserByEmail(String email) { return userRepository.findByEmail(email) .orElseThrow(() -> new ExceptionHandler(ErrorStatus.USER_NOT_FOUND)); @@ -356,5 +269,4 @@ private void updatePlaceImages(Place place, List imageList){ place.updateImages(imageList, s3Service); } } - } diff --git a/src/main/java/com/pickyfy/pickyfy/service/S3Service.java b/src/main/java/com/pickyfy/pickyfy/service/S3Service.java index b74b26b..3df6d0b 100644 --- a/src/main/java/com/pickyfy/pickyfy/service/S3Service.java +++ b/src/main/java/com/pickyfy/pickyfy/service/S3Service.java @@ -26,6 +26,8 @@ public class S3Service { @Value("${cloud.aws.s3.path.image}") private String imageFolder; + // TODO: @Async 비동기 처리 + // TODO: 비동기 처리에 따른 반환값을 CompletableFuture로 수정 public String upload(MultipartFile multipartFile) { String fileName = imageFolder + multipartFile.getOriginalFilename(); @@ -37,11 +39,11 @@ public String upload(MultipartFile multipartFile) { ObjectMetadata metadata = new ObjectMetadata(); metadata.setContentType(multipartFile.getContentType()); metadata.setContentLength(multipartFile.getSize()); - String uploadImageUrl = putS3(multipartFile, fileName, metadata); - - return uploadImageUrl; // 업로드된 파일의 S3 URL 주소 반환 + return putS3(multipartFile, fileName, metadata); // 업로드된 파일의 S3 URL 주소 반환 } + // TODO: @Async 비동기 처리 + // TODO: 비동기 처리에 따른 반환값을 CompletableFuture로 수정 private String putS3(MultipartFile multipartFile, String fileName, ObjectMetadata metadata) { try (InputStream inputStream = multipartFile.getInputStream()) { amazonS3Client.putObject( diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryController.java b/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryController.java index 82e1ba1..0e5926c 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryController.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryController.java @@ -3,8 +3,7 @@ import com.pickyfy.pickyfy.web.apiResponse.common.ApiResponse; import com.pickyfy.pickyfy.service.CategoryService; import com.pickyfy.pickyfy.web.apiResponse.success.SuccessStatus; -import com.pickyfy.pickyfy.web.dto.request.CategoryCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.CategoryUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.CategoryTypeRequest; import com.pickyfy.pickyfy.web.dto.response.CategoryResponse; import jakarta.validation.Valid; import java.util.List; @@ -25,7 +24,7 @@ public class CategoryController implements CategoryControllerApi{ private final CategoryService categoryService; @PostMapping("/admin/category") - public ApiResponse createCategory(@Valid @RequestBody CategoryCreateRequest request) { + public ApiResponse createCategory(@Valid @RequestBody CategoryTypeRequest request) { Long categoryId = categoryService.createCategory(request); return ApiResponse.onSuccess(SuccessStatus.ADD_CATEGORY_SUCCESS, categoryId); } @@ -45,7 +44,7 @@ public ApiResponse> getAllCategories() { @PutMapping("/admin/category/{id}") public ApiResponse updateCategory( @PathVariable Long id, - @Valid @RequestBody CategoryUpdateRequest request) { + @Valid @RequestBody CategoryTypeRequest request) { categoryService.updateCategory(id, request); return ApiResponse.onSuccess(SuccessStatus.EDIT_CATEGORY_SUCCESS, id); } diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryControllerApi.java b/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryControllerApi.java index 05abf61..6005bc6 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryControllerApi.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/CategoryControllerApi.java @@ -1,8 +1,7 @@ package com.pickyfy.pickyfy.web.controller; import com.pickyfy.pickyfy.web.apiResponse.common.ApiResponse; -import com.pickyfy.pickyfy.web.dto.request.CategoryCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.CategoryUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.CategoryTypeRequest; import com.pickyfy.pickyfy.web.dto.response.CategoryResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.responses.ApiResponses; @@ -19,7 +18,7 @@ public interface CategoryControllerApi { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공") }) @PostMapping("/admin/category") - ApiResponse createCategory(@Valid @RequestBody CategoryCreateRequest request); + ApiResponse createCategory(@Valid @RequestBody CategoryTypeRequest request); @Operation(summary = "카테고리 조회 API", description = "특정 카테고리를 조회하는 API입니다.") @ApiResponses({ @@ -40,7 +39,7 @@ public interface CategoryControllerApi { @io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "COMMON200", description = "OK, 성공") }) @PutMapping("/admin/category/{id}") - ApiResponse updateCategory(@PathVariable Long id, @Valid @RequestBody CategoryUpdateRequest request); + ApiResponse updateCategory(@PathVariable Long id, @Valid @RequestBody CategoryTypeRequest request); @Operation(summary = "카테고리 삭제 API", description = "관리자용 카테고리 삭제 API입니다.") @ApiResponses({ diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineController.java b/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineController.java index e39f6e8..546fcaa 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineController.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineController.java @@ -3,8 +3,7 @@ import com.pickyfy.pickyfy.web.apiResponse.common.ApiResponse; import com.pickyfy.pickyfy.service.MagazineService; import com.pickyfy.pickyfy.web.apiResponse.success.SuccessStatus; -import com.pickyfy.pickyfy.web.dto.request.MagazineCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.MagazineUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.MagazineRequest; import com.pickyfy.pickyfy.web.dto.response.MagazineResponse; import jakarta.validation.Valid; import java.util.List; @@ -27,7 +26,7 @@ public class MagazineController implements MagazineControllerApi { private final MagazineService magazineService; @PostMapping(value = "/admin/magazines", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) - public ApiResponse createMagazine(@Valid @ModelAttribute MagazineCreateRequest request) { + public ApiResponse createMagazine(@Valid @ModelAttribute MagazineRequest request) { Long magazineId = magazineService.createMagazine(request); return ApiResponse.onSuccess(SuccessStatus.ADD_MAGAZINE_SUCCESS, magazineId); } @@ -47,7 +46,7 @@ public ApiResponse> getAllMagazines() { @PutMapping(value = "/admin/magazines/{id}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse updateMagazine( @PathVariable Long id, - @Valid @ModelAttribute MagazineUpdateRequest request) { + @Valid @ModelAttribute MagazineRequest request) { magazineService.updateMagazine(id, request); return ApiResponse.onSuccess(SuccessStatus.EDIT_MAGAZINE_SUCCESS, id); } diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineControllerApi.java b/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineControllerApi.java index eb52ed3..a061ce7 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineControllerApi.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/MagazineControllerApi.java @@ -1,8 +1,7 @@ package com.pickyfy.pickyfy.web.controller; import com.pickyfy.pickyfy.web.apiResponse.common.ApiResponse; -import com.pickyfy.pickyfy.web.dto.request.MagazineCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.MagazineUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.MagazineRequest; import com.pickyfy.pickyfy.web.dto.response.MagazineResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -35,7 +34,7 @@ public interface MagazineControllerApi { @PostMapping("/admin/magazines") ApiResponse createMagazine( @Parameter(description = "매거진 생성 정보", required = true) - @Valid @RequestBody MagazineCreateRequest request + @Valid @RequestBody MagazineRequest request ); @Operation( @@ -97,7 +96,7 @@ ApiResponse updateMagazine( @Parameter(description = "매거진 ID", required = true) @PathVariable Long id, @Parameter(description = "매거진 수정 정보", required = true) - @Valid @RequestBody MagazineUpdateRequest request + @Valid @RequestBody MagazineRequest request ); @Operation( diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceController.java b/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceController.java index 91737f7..897208a 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceController.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceController.java @@ -59,8 +59,8 @@ public ApiResponse> searchNearbyPlaces(@RequestBody Ne @PostMapping(value = "/admin/places", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse createPlace( - @RequestPart(value = "image", required = false) List images, - @RequestPart PlaceCreateRequest request) { + @Valid @RequestPart PlaceCreateRequest request, + @RequestPart(value = "image", required = false) List images) { Long id = placeService.createPlace(request, images); return ApiResponse.onSuccess(SuccessStatus.ADD_PLACE_SUCCESS, id); } @@ -68,7 +68,7 @@ public ApiResponse createPlace( @PatchMapping(value = "/admin/places/{placeId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ApiResponse updatePlace( @PathVariable Long placeId, - @RequestPart("request") @Valid PlaceCreateRequest request, + @Valid @RequestPart("request") PlaceCreateRequest request, @RequestPart(value = "image", required = false) List images) { Long id = placeService.updatePlace(placeId, request, images); return ApiResponse.onSuccess(SuccessStatus.EDIT_PLACE_SUCCESS, id); diff --git a/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceControllerApi.java b/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceControllerApi.java index ae2b19d..c2cfe80 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceControllerApi.java +++ b/src/main/java/com/pickyfy/pickyfy/web/controller/PlaceControllerApi.java @@ -78,8 +78,8 @@ ApiResponse> searchNearbyPlaces( }) @PostMapping(("/admin/places")) ApiResponse createPlace( - @RequestPart(value = "image", required = false) List images, - @RequestPart PlaceCreateRequest request); + @Valid @RequestPart PlaceCreateRequest request, + @RequestPart(value = "image", required = false) List images); @Operation(summary = "Place 수정", description = "Place 를 수정하고 5장의 PlaceImage 를 수정하여 업로드합니다.") @@ -89,7 +89,7 @@ ApiResponse createPlace( @PatchMapping(value = "/admin/places/{placeId}") ApiResponse updatePlace( @PathVariable Long placeId, - @RequestPart("request") @Valid PlaceCreateRequest request, + @Valid @RequestPart("request") PlaceCreateRequest request, @RequestPart(value = "image", required = false) List images); diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/PlaceSearchResponseParams.java b/src/main/java/com/pickyfy/pickyfy/web/dto/PlaceSearchResponseParams.java new file mode 100644 index 0000000..826d2e7 --- /dev/null +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/PlaceSearchResponseParams.java @@ -0,0 +1,13 @@ +package com.pickyfy.pickyfy.web.dto; + +import com.pickyfy.pickyfy.domain.Place; + +import java.util.List; + +public record PlaceSearchResponseParams( + Place place, + List placeImageUrls, + String categoryName, + MagazineInfo magazineInfo +) { +} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryCreateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryTypeRequest.java similarity index 63% rename from src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryCreateRequest.java rename to src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryTypeRequest.java index cc2dcc0..69977cb 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryCreateRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryTypeRequest.java @@ -3,7 +3,7 @@ import com.pickyfy.pickyfy.domain.CategoryType; import jakarta.validation.constraints.NotNull; -public record CategoryCreateRequest( - @NotNull(message = "Category type is required") +public record CategoryTypeRequest( + @NotNull(message = "카테고리 타입은 필수입니다.") CategoryType categoryType ) {} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryUpdateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryUpdateRequest.java deleted file mode 100644 index a494433..0000000 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/CategoryUpdateRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.pickyfy.pickyfy.web.dto.request; - -import com.pickyfy.pickyfy.domain.CategoryType; -import jakarta.validation.constraints.NotNull; - -public record CategoryUpdateRequest( - @NotNull(message = "Category type is required") - CategoryType categoryType -) {} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationSendRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationSendRequest.java index 17a16f0..b6adcea 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationSendRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationSendRequest.java @@ -1,7 +1,8 @@ package com.pickyfy.pickyfy.web.dto.request; import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; public record EmailVerificationSendRequest( - @Email String email + @NotNull @Email String email ){} \ No newline at end of file diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationVerifyRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationVerifyRequest.java index 60b3e82..3b08587 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationVerifyRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/EmailVerificationVerifyRequest.java @@ -1,5 +1,8 @@ package com.pickyfy.pickyfy.web.dto.request; +import com.pickyfy.pickyfy.common.AllFieldsNotNull; + +@AllFieldsNotNull public record EmailVerificationVerifyRequest( String email, String code diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineCreateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineRequest.java similarity index 87% rename from src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineCreateRequest.java rename to src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineRequest.java index 2751d32..6a02162 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineCreateRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineRequest.java @@ -3,9 +3,8 @@ import jakarta.validation.constraints.NotBlank; import org.springframework.web.multipart.MultipartFile; -public record MagazineCreateRequest( +public record MagazineRequest( @NotBlank(message = "제목은 필수입니다") String title, - MultipartFile iconFile ) {} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineUpdateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineUpdateRequest.java deleted file mode 100644 index 9559b6c..0000000 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/MagazineUpdateRequest.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.pickyfy.pickyfy.web.dto.request; - -import jakarta.validation.constraints.NotBlank; -import org.springframework.web.multipart.MultipartFile; - -public record MagazineUpdateRequest( - @NotBlank(message = "제목은 필수입니다") - String title, - - MultipartFile iconFile -) {} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PasswordResetRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/PasswordResetRequest.java index c17e060..f81fc7e 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PasswordResetRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/PasswordResetRequest.java @@ -1,17 +1,16 @@ package com.pickyfy.pickyfy.web.dto.request; +import com.pickyfy.pickyfy.common.AllFieldsNotNull; import com.pickyfy.pickyfy.common.Constant; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Pattern; +@AllFieldsNotNull public record PasswordResetRequest( @Email String email, - - @Pattern(regexp = Constant.PASSWORD_REGX, - message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 최소 8자 이상이어야 합니다.") + @Pattern(regexp = Constant.PASSWORD_REGX, message = "비밀번호는 영문, 숫자, 특수문자를 포함하여 최소 8자 이상이어야 합니다.") String newPassword, - String emailToken ) { } diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceCreateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceCreateRequest.java index 2ee72f9..87b6ffd 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceCreateRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceCreateRequest.java @@ -1,14 +1,10 @@ package com.pickyfy.pickyfy.web.dto.request; -import com.pickyfy.pickyfy.domain.Category; -import com.pickyfy.pickyfy.domain.CategoryType; -import com.pickyfy.pickyfy.domain.Magazine; -import com.pickyfy.pickyfy.domain.PlaceImage; -import lombok.Setter; +import com.pickyfy.pickyfy.common.AllFieldsNotNull; import java.math.BigDecimal; -import java.util.List; +@AllFieldsNotNull public record PlaceCreateRequest ( String name, String shortDescription, diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceSearchRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceSearchRequest.java deleted file mode 100644 index 2e4b8fe..0000000 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/PlaceSearchRequest.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.pickyfy.pickyfy.web.dto.request; - -import lombok.Builder; - - -@Builder -public record PlaceSearchRequest ( - Long userId){ - -} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/SavedPlaceRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/SavedPlaceRequest.java deleted file mode 100644 index a28089e..0000000 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/SavedPlaceRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.pickyfy.pickyfy.web.dto.request; - - -public record SavedPlaceRequest ( - String name, - String description, - boolean isPublic, - Long userId -){} diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/request/UserCreateRequest.java b/src/main/java/com/pickyfy/pickyfy/web/dto/request/UserCreateRequest.java index ffb67ca..ed06a7f 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/request/UserCreateRequest.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/request/UserCreateRequest.java @@ -1,9 +1,11 @@ package com.pickyfy.pickyfy.web.dto.request; +import com.pickyfy.pickyfy.common.AllFieldsNotNull; import com.pickyfy.pickyfy.common.Constant; import jakarta.validation.constraints.Email; import jakarta.validation.constraints.Pattern; +@AllFieldsNotNull public record UserCreateRequest( @Pattern(regexp = Constant.NICKNAME_REGX, message = "닉네임은 1~8자, 한글, 영어, 숫자, 특수문자만 허용됩니다.") String nickname, diff --git a/src/main/java/com/pickyfy/pickyfy/web/dto/response/PlaceSearchResponse.java b/src/main/java/com/pickyfy/pickyfy/web/dto/response/PlaceSearchResponse.java index af062bd..1871fb3 100644 --- a/src/main/java/com/pickyfy/pickyfy/web/dto/response/PlaceSearchResponse.java +++ b/src/main/java/com/pickyfy/pickyfy/web/dto/response/PlaceSearchResponse.java @@ -1,26 +1,50 @@ package com.pickyfy.pickyfy.web.dto.response; +import com.pickyfy.pickyfy.domain.Place; +import com.pickyfy.pickyfy.domain.PlaceImage; +import com.pickyfy.pickyfy.web.dto.MagazineInfo; +import com.pickyfy.pickyfy.web.dto.PlaceSearchResponseParams; import lombok.Builder; +import lombok.Getter; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.List; +@Getter @Builder -public record PlaceSearchResponse( - Long placeId, - String name, - String shortDescription, - String instagramLink, - BigDecimal latitude, - BigDecimal longitude, - LocalDateTime createdAt, - LocalDateTime updatedAt, - List placeImageUrl, - List placeImageId, - String categoryName, - String magazineTitle, - String naverLink, - String iconUrl -) { +public class PlaceSearchResponse{ + private Long placeId; + private String name; + private String shortDescription; + private String instagramLink; + private BigDecimal latitude; + private BigDecimal longitude; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + private List placeImageUrl; + private List placeImageId; + private String categoryName; + private String magazineTitle; + private String naverLink; + private String magazineIconUrl; + + public static PlaceSearchResponse from(PlaceSearchResponseParams params){ + return PlaceSearchResponse.builder() + .placeId(params.place().getId()) + .name(params.place().getName()) + .shortDescription(params.place().getShortDescription()) + .latitude(params.place().getLatitude()) + .longitude(params.place().getLongitude()) + .createdAt(params.place().getCreatedAt()) + .updatedAt(params.place().getUpdatedAt()) + .placeImageUrl(params.placeImageUrls()) + .categoryName(params.categoryName()) + .magazineTitle(params.magazineInfo().title()) + .instagramLink(params.place().getInstagramLink()) + .naverLink(params.place().getNaverplaceLink()) + .magazineIconUrl(params.magazineInfo().iconUrl()) + .placeImageId(params.place().getPlaceImages().stream().map(PlaceImage::getId).toList()) + .build(); + } } diff --git a/src/test/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidatorTest.java b/src/test/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidatorTest.java new file mode 100644 index 0000000..458a2f8 --- /dev/null +++ b/src/test/java/com/pickyfy/pickyfy/common/util/AllFieldsNotNullValidatorTest.java @@ -0,0 +1,56 @@ +package com.pickyfy.pickyfy.common.util; + +import com.pickyfy.pickyfy.common.AllFieldsNotNull; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.*; + +class AllFieldsNotNullValidatorTest { + + private Validator validator; + + @BeforeEach + void setUp() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } + + @AllFieldsNotNull + record TestClass (String name, Integer age, List ids){} + + @Test + void succeed_when_all_fields_are_not_null(){ + TestClass validObject = new TestClass("피키파이", 20, List.of(1,2,3)); + Set> violations = validator.validate(validObject); + assertTrue(violations.isEmpty()); + } + + @Test + void fail_when_some_fields_are_null() { + TestClass invalidObject = new TestClass(null, 30, null); + Set> violations = validator.validate(invalidObject); + assertFalse(violations.isEmpty()); + } + + @Test + void fail_when_all_fields_are_null() { + TestClass invalidObject = new TestClass(null, null, null); + Set> violations = validator.validate(invalidObject); + assertFalse(violations.isEmpty()); + } + + @Test + void fail_when_object_is_null() { + TestClass nullObject = null; + assertThrows(IllegalArgumentException.class, () -> validator.validate(nullObject)); + } + +} diff --git a/src/test/java/com/pickyfy/pickyfy/service/CategoryServiceImplTest.java b/src/test/java/com/pickyfy/pickyfy/service/CategoryServiceImplTest.java index 6641997..ec46d12 100644 --- a/src/test/java/com/pickyfy/pickyfy/service/CategoryServiceImplTest.java +++ b/src/test/java/com/pickyfy/pickyfy/service/CategoryServiceImplTest.java @@ -10,8 +10,7 @@ import com.pickyfy.pickyfy.domain.CategoryType; import com.pickyfy.pickyfy.exception.DuplicateResourceException; import com.pickyfy.pickyfy.repository.CategoryRepository; -import com.pickyfy.pickyfy.web.dto.request.CategoryCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.CategoryUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.CategoryTypeRequest; import com.pickyfy.pickyfy.web.dto.response.CategoryResponse; import jakarta.persistence.EntityNotFoundException; import java.lang.reflect.Field; @@ -50,7 +49,7 @@ void setUp() throws NoSuchFieldException, IllegalAccessException { @Test void createCategory_Success() { // Given - CategoryCreateRequest request = new CategoryCreateRequest(CategoryType.CAFE_BAKERY); + CategoryTypeRequest request = new CategoryTypeRequest(CategoryType.CAFE_BAKERY); given(categoryRepository.existsByType(request.categoryType())).willReturn(false); given(categoryRepository.save(any(Category.class))).willReturn(category); @@ -65,7 +64,7 @@ void createCategory_Success() { @Test void createCategory_DuplicateType_ThrowsException() { // Given - CategoryCreateRequest request = new CategoryCreateRequest(CategoryType.CAFE_BAKERY); + CategoryTypeRequest request = new CategoryTypeRequest(CategoryType.CAFE_BAKERY); given(categoryRepository.existsByType(request.categoryType())).willReturn(true); // When/Then @@ -105,14 +104,14 @@ void getAllCategories_Success() { // Then assertThat(responses).hasSize(1); - assertThat(responses.get(0).id()).isEqualTo(1L); - assertThat(responses.get(0).name()).isEqualTo(CategoryType.CAFE_BAKERY.getDisplayName()); + assertThat(responses.getFirst().id()).isEqualTo(1L); + assertThat(responses.getFirst().name()).isEqualTo(CategoryType.CAFE_BAKERY.getDisplayName()); } @Test void updateCategory_Success() { // Given - CategoryUpdateRequest request = new CategoryUpdateRequest(CategoryType.RESTAURANT); + CategoryTypeRequest request = new CategoryTypeRequest(CategoryType.RESTAURANT); given(categoryRepository.findById(1L)).willReturn(Optional.of(category)); given(categoryRepository.existsByType(request.categoryType())).willReturn(false); @@ -126,7 +125,7 @@ void updateCategory_Success() { @Test void updateCategory_DuplicateType_ThrowsException() { // Given - CategoryUpdateRequest request = new CategoryUpdateRequest(CategoryType.RESTAURANT); + CategoryTypeRequest request = new CategoryTypeRequest(CategoryType.RESTAURANT); given(categoryRepository.findById(1L)).willReturn(Optional.of(category)); given(categoryRepository.existsByType(request.categoryType())).willReturn(true); diff --git a/src/test/java/com/pickyfy/pickyfy/service/MagazineServiceImplTest.java b/src/test/java/com/pickyfy/pickyfy/service/MagazineServiceImplTest.java index c9214ff..ffb9cc0 100644 --- a/src/test/java/com/pickyfy/pickyfy/service/MagazineServiceImplTest.java +++ b/src/test/java/com/pickyfy/pickyfy/service/MagazineServiceImplTest.java @@ -9,8 +9,7 @@ import com.pickyfy.pickyfy.domain.Magazine; import com.pickyfy.pickyfy.repository.MagazineRepository; -import com.pickyfy.pickyfy.web.dto.request.MagazineCreateRequest; -import com.pickyfy.pickyfy.web.dto.request.MagazineUpdateRequest; +import com.pickyfy.pickyfy.web.dto.request.MagazineRequest; import com.pickyfy.pickyfy.web.dto.response.MagazineResponse; import jakarta.persistence.EntityNotFoundException; import java.lang.reflect.Field; @@ -68,7 +67,7 @@ void createMagazine_Success() { "test".getBytes() // 간단한 테스트 데이터 ); - MagazineCreateRequest request = new MagazineCreateRequest( + MagazineRequest request = new MagazineRequest( "테스트 매거진", mockFile ); @@ -87,7 +86,7 @@ void createMagazine_Success() { void updateMagazine_SuccessWithJustTitle() { // Given Magazine existingMagazine = new Magazine("기존 매거진", "test-icon.png"); - MagazineUpdateRequest request = new MagazineUpdateRequest( + MagazineRequest request = new MagazineRequest( "수정된 매거진", null ); diff --git a/src/test/java/com/pickyfy/pickyfy/service/PlaceServiceImplTest.java b/src/test/java/com/pickyfy/pickyfy/service/PlaceServiceImplTest.java index fe360a0..e4667eb 100644 --- a/src/test/java/com/pickyfy/pickyfy/service/PlaceServiceImplTest.java +++ b/src/test/java/com/pickyfy/pickyfy/service/PlaceServiceImplTest.java @@ -1,293 +1,287 @@ -//package com.pickyfy.pickyfy.service; -// -//import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; -// -//import com.pickyfy.pickyfy.domain.*; -//import com.pickyfy.pickyfy.domain.UserSavedPlace; -//import com.pickyfy.pickyfy.repository.CategoryRepository; -//import com.pickyfy.pickyfy.repository.MagazineRepository; -//import com.pickyfy.pickyfy.repository.PlaceCategoryRepository; -//import com.pickyfy.pickyfy.repository.PlaceMagazineRepository; -//import com.pickyfy.pickyfy.repository.PlaceRepository; -//import com.pickyfy.pickyfy.repository.UserSavedPlaceRepository; -//import com.pickyfy.pickyfy.repository.UserRepository; -//import com.pickyfy.pickyfy.web.dto.NearbyPlaceSearchCondition; -//import com.pickyfy.pickyfy.web.dto.request.NearbyPlaceSearchRequest; -//import com.pickyfy.pickyfy.web.dto.response.PlaceSearchResponse; -//import java.math.BigDecimal; -//import java.util.List; -//import org.junit.jupiter.api.DisplayName; -//import org.junit.jupiter.api.Test; -//import org.springframework.beans.factory.annotation.Autowired; -//import org.springframework.boot.test.context.SpringBootTest; -//import org.springframework.transaction.annotation.Transactional; -// -//@SpringBootTest -//@Transactional -//class PlaceServiceImplTest { -// -// @Autowired -// private PlaceService placeService; -// -// @Autowired -// private PlaceRepository placeRepository; -// -// @Autowired -// private SavedPlaceRepository savedPlaceRepository; -// -// @Autowired -// private UserRepository userRepository; -// -// @Autowired -// private CategoryRepository categoryRepository; -// -// @Autowired -// private MagazineRepository magazineRepository; -// -// @Autowired -// private PlaceCategoryRepository placeCategoryRepository; -// -// @Autowired -// private PlaceMagazineRepository placeMagazineRepository; -// -// @Autowired -// private UserSavedPlaceRepository userSavedPlaceRepository; -// -// @Test -// @DisplayName("유저가 저장한 장소 목록을 성공적으로 조회한다") -// void getUserSavePlace_Success() { -// // Given -// Category category = createCategory(CategoryType.RESTAURANT); -// Magazine magazine = createMagazine("테스트 매거진"); -// User user = createUser(); -// Place place = createPlaceWithRelations( -// "테스트 장소", -// BigDecimal.valueOf(37.5665), -// BigDecimal.valueOf(126.9780), -// category, -// magazine -// ); -// -// SavedPlace savedPlace = createSavedPlace(place, user); -// createPlaceSavedPlace(place, savedPlace); -// -// // When -// List result = placeService.getUserSavePlace(user.getEmail()); -// -// // Then -// assertThat(result).hasSize(1); -// PlaceSearchResponse response = result.get(0); -// assertThat(response) -// .satisfies(r -> { -// assertThat(r.placeId()).isEqualTo(place.getId()); -// assertThat(r.name()).isEqualTo("테스트 장소"); -// assertThat(r.shortDescription()).isEqualTo("테스트 설명"); -// assertThat(r.categoryName()).isEqualTo("음식점"); -// assertThat(r.magazineTitle()).isEqualTo("테스트 매거진"); -// assertThat(r.placeImageUrl()).containsExactly("test-image-url.jpg"); -// assertThat(r.instagramLink()).isEqualTo("instagram.com"); -// assertThat(r.naverLink()).isEqualTo("naver.com"); -// }); -// } -// -// @Test -// @DisplayName("유저가 저장한 장소가 없는 경우 빈 리스트를 반환한다") -// void getUserSavePlace_EmptyList() { -// // Given -// User user = createUser(); -// -// // When -// List result = placeService.getUserSavePlace(user.getEmail()); -// -// // Then -// assertThat(result).isEmpty(); -// } -// -// @Test -// @DisplayName("특정 플레이스를 성공적으로 조회한다") -// void getPlace_Success() { -// // Given -// Category category = createCategory(CategoryType.RESTAURANT); -// Magazine magazine = createMagazine("테스트 매거진"); -// -// Place place = createPlaceWithRelations( -// "테스트 장소", -// BigDecimal.valueOf(37.5665), -// BigDecimal.valueOf(126.9780), -// category, -// magazine -// ); -// -// // When -// PlaceSearchResponse result = placeService.getPlace(place.getId()); -// -// // Then -// assertThat(result) -// .satisfies(r -> { -// assertThat(r.placeId()).isEqualTo(place.getId()); -// assertThat(r.name()).isEqualTo("테스트 장소"); -// assertThat(r.shortDescription()).isEqualTo("테스트 설명"); -// assertThat(r.categoryName()).isEqualTo("음식점"); -// assertThat(r.magazineTitle()).isEqualTo("테스트 매거진"); -// assertThat(r.placeImageUrl()).containsExactly("test-image-url.jpg"); -// assertThat(r.instagramLink()).isEqualTo("instagram.com"); -// assertThat(r.naverLink()).isEqualTo("naver.com"); -// }); -// } -// -// @Test -// @DisplayName("장소 저장 및 저장 취소를 성공적으로 토글한다") -// void togglePlaceUser_Success() { -// // Given -// User user = createUser(); -// Place place = createPlace("테스트 장소", -// BigDecimal.valueOf(37.5665), -// BigDecimal.valueOf(126.9780)); -// -// // When -// boolean firstToggle = placeService.togglePlaceUser(user.getEmail(), place.getId()); -// -// // Then -// assertThat(firstToggle).isTrue(); -// assertThat(savedPlaceRepository.findByUserIdAndName(user.getId(), place.getName())).isPresent(); -// -// // When - 두 번째 토글 (저장 취소) -// boolean secondToggle = placeService.togglePlaceUser(user.getEmail(), place.getId()); -// -// // Then -// assertThat(secondToggle).isFalse(); -// assertThat(savedPlaceRepository.findByUserIdAndName(user.getId(), place.getName())).isEmpty(); -// } -// -// @Test -// @DisplayName("근처 장소를 성공적으로 검색한다") -// void searchNearbyPlaces_Success() { -// // Given -// Category category = createCategory(CategoryType.RESTAURANT); -// Magazine magazine = createMagazine("테스트 매거진"); -// -// // 가까운 장소와 먼 장소 생성 -// Place nearPlace = createPlaceWithRelations( -// "가까운 장소", -// BigDecimal.valueOf(12.3456789), -// BigDecimal.valueOf(98.7654321), -// category, -// magazine -// ); -// -// Place farPlace = createPlaceWithRelations( -// "먼 장소", -// BigDecimal.valueOf(12.3556789), -// BigDecimal.valueOf(98.7754321), -// category, -// magazine -// ); -// -// // When - 가까운 장소의 위경도를 기준으로 500m 반경 검색 -// NearbyPlaceSearchRequest request = new NearbyPlaceSearchRequest(BigDecimal.valueOf(12.3456789), -// BigDecimal.valueOf(98.7654321), -// 500.0, // 500m 반경 -// List.of(category.getId()), -// List.of(magazine.getId())); -// -// List result = placeService.searchNearbyPlaces(request); -// -// // Then -// assertThat(result).hasSize(1); -// assertThat(result.getFirst().getName()).isEqualTo("가까운 장소"); -// } -// -// // 테스트 데이터 생성 헬퍼 메서드들 -// private User createUser() { -// User user = User.builder() -// .email("test@example.com") -// .provider(Provider.EMAIL) -// .nickname("테스터") -// .build(); -// return userRepository.save(user); -// } -// -// private Category createCategory(CategoryType type) { -// Category category = Category.builder() -// .type(type) -// .build(); -// return categoryRepository.save(category); -// } -// -// private Magazine createMagazine(String title) { -// Magazine magazine = Magazine.builder() -// .title(title) -// .build(); -// return magazineRepository.save(magazine); -// } -// -// private Place createPlace(String name, BigDecimal latitude, BigDecimal longitude) { -// Place place = Place.builder() -// .name(name) -// .address("테스트 주소") -// .shortDescription("테스트 설명") -// .latitude(latitude) -// .longitude(longitude) -// .instagramLink("instagram.com") -// .naverplaceLink("naver.com") -// .build(); -// return placeRepository.save(place); -// } -// -// private PlaceCategory createPlaceCategory(Place place, Category category) { -// PlaceCategory placeCategory = PlaceCategory.builder() -// .place(place) -// .category(category) -// .build(); -// return placeCategoryRepository.save(placeCategory); -// } -// -// private PlaceMagazine createPlaceMagazine(Place place, Magazine magazine) { -// PlaceMagazine placeMagazine = PlaceMagazine.builder() -// .place(place) -// .magazine(magazine) -// .build(); -// return placeMagazineRepository.save(placeMagazine); -// } -// -// private UserSavedPlace createSavedPlace(Place place, User user) { -// UserSavedPlace savedPlace = UserSavedPlace.builder() -// .name(place.getName()) -// .description(place.getShortDescription()) -// .isPublic(true) -// .user(user) -// .build(); -// return savedPlaceRepository.save(savedPlace); -// } -// -// private UserSavedPlace createPlaceSavedPlace(Place place, SavedPlace savedPlace) { -// UserSavedPlace userSavedPlace = UserSavedPlace.builder() -// .place(place) -// .savedPlace(savedPlace) -// .build(); -// return userSavedPlaceRepository.save(userSavedPlace); -// } -// -// private PlaceImage createPlaceImage(Place place, String url) { -// PlaceImage placeImage = PlaceImage.builder() -// .place(place) -// .url(url) -// .sequence(1) -// .build(); -// place.getPlaceImages().add(placeImage); -// return placeRepository.save(place).getPlaceImages().get(0); -// } -// -// // 복합 생성 헬퍼 메서드 -// private Place createPlaceWithRelations(String name, BigDecimal latitude, BigDecimal longitude, -// Category category, Magazine magazine) { -// Place place = createPlace(name, latitude, longitude); -// -// createPlaceCategory(place, category); -// createPlaceMagazine(place, magazine); -// createPlaceImage(place, "test-image-url.jpg"); -// -// return place; -// } -//} -// +package com.pickyfy.pickyfy.service; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +import com.pickyfy.pickyfy.domain.*; +import com.pickyfy.pickyfy.domain.UserSavedPlace; +import com.pickyfy.pickyfy.repository.CategoryRepository; +import com.pickyfy.pickyfy.repository.MagazineRepository; +import com.pickyfy.pickyfy.repository.PlaceCategoryRepository; +import com.pickyfy.pickyfy.repository.PlaceMagazineRepository; +import com.pickyfy.pickyfy.repository.PlaceRepository; +import com.pickyfy.pickyfy.repository.UserSavedPlaceRepository; +import com.pickyfy.pickyfy.repository.UserRepository; +import com.pickyfy.pickyfy.web.dto.request.NearbyPlaceSearchRequest; +import com.pickyfy.pickyfy.web.dto.response.PlaceSearchResponse; +import java.math.BigDecimal; +import java.util.List; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.transaction.annotation.Transactional; + +@SpringBootTest +@Transactional +class PlaceServiceImplTest { + + @Autowired + private PlaceService placeService; + + @Autowired + private PlaceRepository placeRepository; + + @Autowired + private UserRepository userRepository; + + @Autowired + private CategoryRepository categoryRepository; + + @Autowired + private MagazineRepository magazineRepository; + + @Autowired + private PlaceCategoryRepository placeCategoryRepository; + + @Autowired + private PlaceMagazineRepository placeMagazineRepository; + + @Autowired + private UserSavedPlaceRepository userSavedPlaceRepository; + + @Test + @DisplayName("유저가 저장한 장소 목록을 성공적으로 조회한다") + void getUserSavePlace_Success() { + // Given + Category category = createCategory(CategoryType.RESTAURANT); + Magazine magazine = createMagazine("테스트 매거진"); + User user = createUser(); + Place place = createPlaceWithRelations( + "테스트 장소", + BigDecimal.valueOf(37.5665), + BigDecimal.valueOf(126.9780), + category, + magazine + ); + + createPlaceSavedPlace(place, user); + + // When + List result = placeService.getUserSavePlace(user.getEmail()); + + // Then + assertThat(result).hasSize(1); + PlaceSearchResponse response = result.get(0); + assertThat(response) + .satisfies(r -> { + assertThat(r.getPlaceId()).isEqualTo(place.getId()); + assertThat(r.getName()).isEqualTo("테스트 장소"); + assertThat(r.getShortDescription()).isEqualTo("테스트 설명"); + assertThat(r.getCategoryName()).isEqualTo("음식점"); + assertThat(r.getMagazineTitle()).isEqualTo("테스트 매거진"); + assertThat(r.getPlaceImageUrl()).containsExactly("test-image-url.jpg"); + assertThat(r.getInstagramLink()).isEqualTo("instagram.com"); + assertThat(r.getNaverLink()).isEqualTo("naver.com"); + }); + } + + @Test + @DisplayName("유저가 저장한 장소가 없는 경우 빈 리스트를 반환한다") + void getUserSavePlace_EmptyList() { + // Given + User user = createUser(); + + // When + List result = placeService.getUserSavePlace(user.getEmail()); + + // Then + assertThat(result).isEmpty(); + } + + @Test + @DisplayName("특정 플레이스를 성공적으로 조회한다") + void getPlace_Success() { + // Given + Category category = createCategory(CategoryType.RESTAURANT); + Magazine magazine = createMagazine("테스트 매거진"); + + Place place = createPlaceWithRelations( + "테스트 장소", + BigDecimal.valueOf(37.5665), + BigDecimal.valueOf(126.9780), + category, + magazine + ); + + // When + PlaceSearchResponse result = placeService.getPlace(place.getId()); + + // Then + assertThat(result) + .satisfies(r -> { + assertThat(r.getPlaceId()).isEqualTo(place.getId()); + assertThat(r.getName()).isEqualTo("테스트 장소"); + assertThat(r.getShortDescription()).isEqualTo("테스트 설명"); + assertThat(r.getCategoryName()).isEqualTo("음식점"); + assertThat(r.getMagazineTitle()).isEqualTo("테스트 매거진"); + assertThat(r.getPlaceImageUrl()).containsExactly("test-image-url.jpg"); + assertThat(r.getInstagramLink()).isEqualTo("instagram.com"); + assertThat(r.getNaverLink()).isEqualTo("naver.com"); + }); + } + + @Test + @DisplayName("장소 저장 및 저장 취소를 성공적으로 토글한다") + void togglePlaceUser_Success() { + // Given + User user = createUser(); + Place place = createPlace("테스트 장소", + BigDecimal.valueOf(37.5665), + BigDecimal.valueOf(126.9780)); + + // When + boolean firstToggle = placeService.togglePlaceUser(user.getEmail(), place.getId()); + + // Then + assertThat(firstToggle).isTrue(); + assertThat(userSavedPlaceRepository.findByUserIdAndPlaceId(user.getId(), place.getId())).isPresent(); + + // When - 두 번째 토글 (저장 취소) + boolean secondToggle = placeService.togglePlaceUser(user.getEmail(), place.getId()); + + // Then + assertThat(secondToggle).isFalse(); + assertThat(userSavedPlaceRepository.findByUserIdAndPlaceId(user.getId(), place.getId())).isEmpty(); + } + + @Test + @DisplayName("근처 장소를 성공적으로 검색한다") + void searchNearbyPlaces_Success() { + // Given + Category category = createCategory(CategoryType.RESTAURANT); + Magazine magazine = createMagazine("테스트 매거진"); + + // 가까운 장소와 먼 장소 생성 + Place nearPlace = createPlaceWithRelations( + "가까운 장소", + BigDecimal.valueOf(12.3456789), + BigDecimal.valueOf(98.7654321), + category, + magazine + ); + + Place farPlace = createPlaceWithRelations( + "먼 장소", + BigDecimal.valueOf(12.3556789), + BigDecimal.valueOf(98.7754321), + category, + magazine + ); + + // When - 가까운 장소의 위경도를 기준으로 500m 반경 검색 + NearbyPlaceSearchRequest request = new NearbyPlaceSearchRequest(BigDecimal.valueOf(12.3456789), + BigDecimal.valueOf(98.7654321), + 500.0, // 500m 반경 + List.of(category.getId()), + List.of(magazine.getId())); + + List result = placeService.searchNearbyPlaces(request); + + // Then + assertThat(result).hasSize(1); + assertThat(result.getFirst().getName()).isEqualTo("가까운 장소"); + } + + // 테스트 데이터 생성 헬퍼 메서드들 + private User createUser() { + User user = User.builder() + .email("test@example.com") + .provider(Provider.EMAIL) + .nickname("테스터") + .build(); + return userRepository.save(user); + } + + private Category createCategory(CategoryType type) { + Category category = Category.builder() + .type(type) + .build(); + return categoryRepository.save(category); + } + + private Magazine createMagazine(String title) { + Magazine magazine = Magazine.builder() + .title(title) + .build(); + return magazineRepository.save(magazine); + } + + private Place createPlace(String name, BigDecimal latitude, BigDecimal longitude) { + Place place = Place.builder() + .name(name) + .address("테스트 주소") + .shortDescription("테스트 설명") + .latitude(latitude) + .longitude(longitude) + .instagramLink("instagram.com") + .naverplaceLink("naver.com") + .build(); + return placeRepository.save(place); + } + + private PlaceCategory createPlaceCategory(Place place, Category category) { + PlaceCategory placeCategory = PlaceCategory.builder() + .place(place) + .category(category) + .build(); + return placeCategoryRepository.save(placeCategory); + } + + private PlaceMagazine createPlaceMagazine(Place place, Magazine magazine) { + PlaceMagazine placeMagazine = PlaceMagazine.builder() + .place(place) + .magazine(magazine) + .build(); + return placeMagazineRepository.save(placeMagazine); + } + + private UserSavedPlace createSavedPlace(Place place, User user) { + UserSavedPlace savedPlace = UserSavedPlace.builder() + .user(user) + .place(place) + .build(); + + return userSavedPlaceRepository.save(savedPlace); + } + + private UserSavedPlace createPlaceSavedPlace(Place place, User user) { + UserSavedPlace userSavedPlace = UserSavedPlace.builder() + .place(place) + .user(user) + .build(); + return userSavedPlaceRepository.save(userSavedPlace); + } + + private PlaceImage createPlaceImage(Place place, String url) { + PlaceImage placeImage = PlaceImage.builder() + .place(place) + .url(url) + .sequence(1) + .build(); + place.getPlaceImages().add(placeImage); + return placeRepository.save(place).getPlaceImages().get(0); + } + + // 복합 생성 헬퍼 메서드 + private Place createPlaceWithRelations(String name, BigDecimal latitude, BigDecimal longitude, + Category category, Magazine magazine) { + Place place = createPlace(name, latitude, longitude); + + createPlaceCategory(place, category); + createPlaceMagazine(place, magazine); + createPlaceImage(place, "test-image-url.jpg"); + + return place; + } +} +