diff --git a/build.gradle b/build.gradle index 271586a..5063376 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' + testRuntimeOnly 'com.h2database:h2' /* swagger */ implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.0.4' @@ -57,6 +58,9 @@ dependencies { /* redis */ implementation group: 'org.springframework.boot', name: 'spring-boot-starter-data-redis' + + // H2 Database + implementation 'com.h2database:h2' } tasks.named('test') { diff --git a/src/main/java/server/acode/domain/family/controller/DisplayController.java b/src/main/java/server/acode/domain/family/controller/DisplayController.java index f081335..3d969e1 100644 --- a/src/main/java/server/acode/domain/family/controller/DisplayController.java +++ b/src/main/java/server/acode/domain/family/controller/DisplayController.java @@ -11,9 +11,9 @@ import static org.springframework.util.StringUtils.*; import org.springframework.web.bind.annotation.RestController; import server.acode.domain.family.dto.request.FragranceFilterCond; -import server.acode.domain.family.dto.response.DisplayBrand; -import server.acode.domain.family.dto.response.DisplayFamily; -import server.acode.domain.family.dto.response.DisplayIngredient; +import server.acode.domain.family.dto.response.BrandDetailsDto; +import server.acode.domain.family.dto.response.FamilyDetailsDto; +import server.acode.domain.family.dto.response.IngredientDetailsDto; import server.acode.domain.family.dto.response.PageableResponse; import server.acode.domain.family.service.DisplayService; import server.acode.global.common.PageRequest; @@ -31,31 +31,31 @@ public class DisplayController { "계열 두 개 검색 시에는 두 계열 사이 공백 한 칸 (url 상으로는 %20) 넣어주세요. " + "페이지는 파라미터 없을 시 기본 page = 1, size = 10입니다.") @GetMapping("/display") - public PageableResponse displayV1(FragranceFilterCond cond, PageRequest pageRequest){ + public PageableResponse displayFragranceV1(FragranceFilterCond cond, PageRequest pageRequest){ // param에 따라 계열별/브랜드별 or 추천향료별 분기 PageableResponse response = hasText(cond.getIngredient()) - ? displayService.searchFragranceListByIngredient(cond.getIngredient(), pageRequest) - : displayService.searchFragranceList(cond, pageRequest); + ? displayService.searchFragranceByIngredient(cond.getIngredient(), pageRequest) + : displayService.searchFragranceByBrandAndFamily(cond, pageRequest); return response; } @Operation(summary = "브랜드 설명") @GetMapping("/display/brand/{brand}") - public DisplayBrand displayBrandV1(@PathVariable("brand") String brand){ + public BrandDetailsDto displayBrandV1(@PathVariable("brand") String brand){ return displayService.getBrandContent(brand); } @Operation(summary = "계열 설명") @GetMapping("/display/family/{family}") - public DisplayFamily displayFamilyV1(@PathVariable("family") String family){ + public FamilyDetailsDto displayFamilyV1(@PathVariable("family") String family){ return displayService.getFamilyContent(family); } @Operation(summary = "향료 설명") @GetMapping("/display/ingredient/{ingredient}") - public DisplayIngredient displayIngredientV1(@PathVariable("ingredient") String ingredient){ + public IngredientDetailsDto displayIngredientV1(@PathVariable("ingredient") String ingredient){ return displayService.getIngredientContent(ingredient); } diff --git a/src/main/java/server/acode/domain/family/controller/HomeController.java b/src/main/java/server/acode/domain/family/controller/HomeController.java index b0ee6eb..7be5d4a 100644 --- a/src/main/java/server/acode/domain/family/controller/HomeController.java +++ b/src/main/java/server/acode/domain/family/controller/HomeController.java @@ -7,7 +7,7 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; -import server.acode.domain.family.dto.response.HomeFragrance; +import server.acode.domain.family.dto.response.HomeFragranceDto; import server.acode.domain.family.service.HomeService; import server.acode.domain.ingredient.dto.response.IngredientOfTheDay; @@ -25,8 +25,8 @@ public class HomeController { @Operation(summary = "계열별 향수 최대 6개", description = "아직 포스터 이미지가 준비되지 않아 우디에만 테스트용으로 넣어놨습니다 우디로 테스트 해주세요") @GetMapping("/home") - public List searchV1(@RequestParam String family){ - return homeService.search(family); + public List searchMainFragranceV1(@RequestParam String family){ + return homeService.searchMainFragrance(family); } @Operation(summary = "오늘의 추천 향료") diff --git a/src/main/java/server/acode/domain/family/dto/request/FragranceFilterCond.java b/src/main/java/server/acode/domain/family/dto/request/FragranceFilterCond.java index 5153c4b..3032c1a 100644 --- a/src/main/java/server/acode/domain/family/dto/request/FragranceFilterCond.java +++ b/src/main/java/server/acode/domain/family/dto/request/FragranceFilterCond.java @@ -8,5 +8,6 @@ public class FragranceFilterCond { private String brand; private String family; + private String additionalFamily; private String ingredient; } diff --git a/src/main/java/server/acode/domain/family/dto/response/DisplayBrand.java b/src/main/java/server/acode/domain/family/dto/response/BrandDetailsDto.java similarity index 65% rename from src/main/java/server/acode/domain/family/dto/response/DisplayBrand.java rename to src/main/java/server/acode/domain/family/dto/response/BrandDetailsDto.java index 586432b..ebc180c 100644 --- a/src/main/java/server/acode/domain/family/dto/response/DisplayBrand.java +++ b/src/main/java/server/acode/domain/family/dto/response/BrandDetailsDto.java @@ -3,25 +3,22 @@ import lombok.Getter; import server.acode.domain.fragrance.entity.Brand; -import java.util.Arrays; -import java.util.List; - @Getter -public class DisplayBrand { +public class BrandDetailsDto { private String korName; private String engName; private String summary; private String background; - private DisplayBrand(String korName, String engName, String summary, String background){ + private BrandDetailsDto(String korName, String engName, String summary, String background){ this.korName = korName; this.engName = engName; this.summary = summary; this.background = background; } - public static DisplayBrand from(Brand brand){ - return new DisplayBrand(brand.getKorName(), + public static BrandDetailsDto from(Brand brand){ + return new BrandDetailsDto(brand.getKorName(), brand.getEngName(), brand.getSummary(), brand.getBackgroundImg()); diff --git a/src/main/java/server/acode/domain/family/dto/response/DisplayFamily.java b/src/main/java/server/acode/domain/family/dto/response/FamilyDetailsDto.java similarity index 74% rename from src/main/java/server/acode/domain/family/dto/response/DisplayFamily.java rename to src/main/java/server/acode/domain/family/dto/response/FamilyDetailsDto.java index f27132f..8792903 100644 --- a/src/main/java/server/acode/domain/family/dto/response/DisplayFamily.java +++ b/src/main/java/server/acode/domain/family/dto/response/FamilyDetailsDto.java @@ -7,7 +7,7 @@ import java.util.List; @Getter -public class DisplayFamily { +public class FamilyDetailsDto { private String korName; private String engName; private List keyword; @@ -15,7 +15,7 @@ public class DisplayFamily { private String background; private String icon; - private DisplayFamily(String korName, String engName, String keyword, String summary, String background, String icon){ + private FamilyDetailsDto(String korName, String engName, String keyword, String summary, String background, String icon){ this.korName = korName; this.engName = engName; this.keyword = Arrays.asList(keyword.split(", ")); @@ -24,8 +24,8 @@ private DisplayFamily(String korName, String engName, String keyword, String sum this.icon = icon; } - public static DisplayFamily from(Family family){ - return new DisplayFamily(family.getKorName(), + public static FamilyDetailsDto from(Family family){ + return new FamilyDetailsDto(family.getKorName(), family.getEngName(), family.getKeyword(), family.getSummary(), diff --git a/src/main/java/server/acode/domain/family/dto/response/DisplayFragrance.java b/src/main/java/server/acode/domain/family/dto/response/FragranceCatalogDto.java similarity index 78% rename from src/main/java/server/acode/domain/family/dto/response/DisplayFragrance.java rename to src/main/java/server/acode/domain/family/dto/response/FragranceCatalogDto.java index 47daaaf..02aaa56 100644 --- a/src/main/java/server/acode/domain/family/dto/response/DisplayFragrance.java +++ b/src/main/java/server/acode/domain/family/dto/response/FragranceCatalogDto.java @@ -5,7 +5,7 @@ import server.acode.domain.fragrance.entity.Concentration; @Data -public class DisplayFragrance { +public class FragranceCatalogDto { private Long fragranceId; private String brandName; // 브랜드 이름 @@ -14,7 +14,7 @@ public class DisplayFragrance { private String concentration; @QueryProjection - public DisplayFragrance (Long fragranceId, String brandName, String fragranceName, String thumbnail, Concentration concentration){ + public FragranceCatalogDto(Long fragranceId, String brandName, String fragranceName, String thumbnail, Concentration concentration){ this.fragranceId = fragranceId; this.brandName = brandName; this.fragranceName = fragranceName; diff --git a/src/main/java/server/acode/domain/family/dto/response/HomeFragrance.java b/src/main/java/server/acode/domain/family/dto/response/HomeFragranceDto.java similarity index 81% rename from src/main/java/server/acode/domain/family/dto/response/HomeFragrance.java rename to src/main/java/server/acode/domain/family/dto/response/HomeFragranceDto.java index 98ade2c..826ca68 100644 --- a/src/main/java/server/acode/domain/family/dto/response/HomeFragrance.java +++ b/src/main/java/server/acode/domain/family/dto/response/HomeFragranceDto.java @@ -7,7 +7,7 @@ import java.util.List; @Data -public class HomeFragrance { +public class HomeFragranceDto { private Long fragranceId; private String fragranceName; private String brandName; @@ -15,7 +15,7 @@ public class HomeFragrance { private String poster; @QueryProjection - public HomeFragrance(Long fragranceId, String fragranceName, String brandName, String style, String poster){ + public HomeFragranceDto(Long fragranceId, String fragranceName, String brandName, String style, String poster){ this.fragranceId = fragranceId; this.fragranceName = fragranceName; this.brandName = brandName; diff --git a/src/main/java/server/acode/domain/family/dto/response/DisplayIngredient.java b/src/main/java/server/acode/domain/family/dto/response/IngredientDetailsDto.java similarity index 67% rename from src/main/java/server/acode/domain/family/dto/response/DisplayIngredient.java rename to src/main/java/server/acode/domain/family/dto/response/IngredientDetailsDto.java index 65da0dc..e7b9017 100644 --- a/src/main/java/server/acode/domain/family/dto/response/DisplayIngredient.java +++ b/src/main/java/server/acode/domain/family/dto/response/IngredientDetailsDto.java @@ -1,7 +1,6 @@ package server.acode.domain.family.dto.response; import lombok.Getter; -import server.acode.domain.family.entity.Family; import server.acode.domain.ingredient.entity.Ingredient; import server.acode.domain.ingredient.entity.IngredientType; @@ -9,7 +8,7 @@ import java.util.List; @Getter -public class DisplayIngredient { +public class IngredientDetailsDto { private String korName; private String engName; private String acode; @@ -19,8 +18,8 @@ public class DisplayIngredient { private String ingredientType; private String icon; - private DisplayIngredient(String korName, String engName, String acode, String keyword, - String summary, String background, String ingredientType,String icon){ + private IngredientDetailsDto(String korName, String engName, String acode, String keyword, + String summary, String background, String ingredientType, String icon){ this.korName = korName; this.engName = engName; this.acode = acode; @@ -31,14 +30,14 @@ private DisplayIngredient(String korName, String engName, String acode, String k this.icon = icon; } - public static DisplayIngredient from(Ingredient ingredient, IngredientType type){ - return new DisplayIngredient(ingredient.getKorName(), + public static IngredientDetailsDto from(Ingredient ingredient){ + return new IngredientDetailsDto(ingredient.getKorName(), ingredient.getEngName(), ingredient.getAcode(), ingredient.getKeyword(), ingredient.getSummary(), ingredient.getBackgroundImg(), - type.getKorName(), - type.getIcon()); + ingredient.getIngredientType().getKorName(), + ingredient.getIngredientType().getIcon()); } } diff --git a/src/main/java/server/acode/domain/family/repository/FamilyRepository.java b/src/main/java/server/acode/domain/family/repository/FamilyRepository.java index 4761486..5890908 100644 --- a/src/main/java/server/acode/domain/family/repository/FamilyRepository.java +++ b/src/main/java/server/acode/domain/family/repository/FamilyRepository.java @@ -5,11 +5,12 @@ import server.acode.domain.family.entity.Family; import java.util.List; +import java.util.Optional; @Repository public interface FamilyRepository extends JpaRepository{ boolean existsByKorName(String korName); - Family findByKorName(String korName); + Optional findByKorName(String korName); List findByIdIn(List familyIdList); } diff --git a/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryCustom.java b/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryCustom.java index 22ea893..e970776 100644 --- a/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryCustom.java +++ b/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryCustom.java @@ -5,8 +5,8 @@ import org.springframework.stereotype.Repository; import server.acode.domain.family.dto.SimilarFragranceOrCond; import server.acode.domain.family.dto.request.FragranceFilterCond; -import server.acode.domain.family.dto.response.DisplayFragrance; -import server.acode.domain.family.dto.response.HomeFragrance; +import server.acode.domain.family.dto.response.FragranceCatalogDto; +import server.acode.domain.family.dto.response.HomeFragranceDto; import server.acode.domain.fragrance.dto.request.SearchCond; import server.acode.domain.fragrance.dto.response.ExtractFragrance; import server.acode.domain.fragrance.dto.response.FamilyCountDto; @@ -16,9 +16,9 @@ @Repository public interface FragranceFamilyRepositoryCustom { - List search(String familyName); + List search(String familyName); - Page searchByFilter(FragranceFilterCond cond, String additionalFamily, Pageable pageable); + Page searchByBrandAndFamily(FragranceFilterCond cond, Pageable pageable); List searchFamilyByFragranceId(Long fragranceId); diff --git a/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryImpl.java b/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryImpl.java index 8cbb22f..168f7ca 100644 --- a/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryImpl.java +++ b/src/main/java/server/acode/domain/family/repository/FragranceFamilyRepositoryImpl.java @@ -13,10 +13,10 @@ import org.springframework.stereotype.Repository; import server.acode.domain.family.dto.SimilarFragranceOrCond; import server.acode.domain.family.dto.request.FragranceFilterCond; -import server.acode.domain.family.dto.response.DisplayFragrance; -import server.acode.domain.family.dto.response.HomeFragrance; -import server.acode.domain.family.dto.response.QDisplayFragrance; -import server.acode.domain.family.dto.response.QHomeFragrance; +import server.acode.domain.family.dto.response.FragranceCatalogDto; +import server.acode.domain.family.dto.response.HomeFragranceDto; +import server.acode.domain.family.dto.response.QFragranceCatalogDto; +import server.acode.domain.family.dto.response.QHomeFragranceDto; import server.acode.domain.family.entity.QFragranceFamily; import server.acode.domain.fragrance.dto.request.SearchCond; import server.acode.domain.fragrance.dto.response.*; @@ -39,9 +39,9 @@ public FragranceFamilyRepositoryImpl(EntityManager em) { } @Override - public List search(String familyName) { + public List search(String familyName) { return queryFactory - .select(new QHomeFragrance( + .select(new QHomeFragranceDto( fragrance.id.as("fragranceId"), fragrance.name.as("fragranceName"), brand.korName.as("korBrand"), @@ -62,11 +62,11 @@ public List search(String familyName) { } @Override - public Page searchByFilter(FragranceFilterCond cond, String additionalFamily, Pageable pageable) { + public Page searchByBrandAndFamily(FragranceFilterCond cond, Pageable pageable) { //TODO 카운트 쿼리 분리 - QueryResults results = queryFactory - .select(new QDisplayFragrance( + QueryResults results = queryFactory + .select(new QFragranceCatalogDto( fragrance.id.as("fragranceId"), brand.korName.as("brandName"), fragrance.name.as("fragranceName"), @@ -79,7 +79,7 @@ public Page searchByFilter(FragranceFilterCond cond, String ad .join(fragrance.brand, brand) .where(brandNameEq(cond.getBrand()), familyNameEq(cond.getFamily()), - additionalFamilyNameEq(additionalFamily) + additionalFamilyNameEq(cond.getAdditionalFamily()) ) .groupBy(fragrance.id) .offset(pageable.getOffset()) @@ -87,7 +87,7 @@ public Page searchByFilter(FragranceFilterCond cond, String ad .fetchResults(); - List content = results.getResults(); + List content = results.getResults(); long total = results.getTotal(); return new PageImpl<>(content, pageable, total); diff --git a/src/main/java/server/acode/domain/family/service/DisplayService.java b/src/main/java/server/acode/domain/family/service/DisplayService.java index bc124c3..29dfece 100644 --- a/src/main/java/server/acode/domain/family/service/DisplayService.java +++ b/src/main/java/server/acode/domain/family/service/DisplayService.java @@ -14,9 +14,7 @@ import server.acode.domain.fragrance.repository.BrandRepository; import server.acode.domain.fragrance.repository.FragranceRepository; import server.acode.domain.ingredient.entity.Ingredient; -import server.acode.domain.ingredient.entity.IngredientType; import server.acode.domain.ingredient.repository.IngredientRepository; -import server.acode.domain.ingredient.repository.IngredientTypeRepository; import server.acode.global.common.ErrorCode; import server.acode.global.common.PageRequest; import server.acode.global.exception.CustomException; @@ -32,60 +30,53 @@ public class DisplayService { private final FamilyRepository familyRepository; private final BrandRepository brandRepository; private final IngredientRepository ingredientRepository; - private final IngredientTypeRepository ingredientTypeRepository; private final FragranceFamilyRepository fragranceFamilyRepository; private final FragranceRepository fragranceRepository; - public PageableResponse searchFragranceList(FragranceFilterCond cond, PageRequest pageRequest){ + public PageableResponse searchFragranceByBrandAndFamily(FragranceFilterCond cond, PageRequest pageRequest){ Pageable pageable = pageRequest.of(); + cond = parseFilterCond(cond); - String additionalFamily = null; // 추가 계열 변수 초기화 - // family에 값이 들어온 경우 값 처리 + Page result = fragranceFamilyRepository.searchByBrandAndFamily(cond, pageable); // 향수 조회 + return new PageableResponse(result.getContent(), result.getTotalPages(), result.getTotalElements()); + } + + private FragranceFilterCond parseFilterCond(FragranceFilterCond cond){ if(hasText(cond.getFamily())) { String[] parts = cond.getFamily().split("\\s+", 2); cond.setFamily(parts.length > 0 ? parts[0] : null); - additionalFamily = parts.length > 1 ? parts[1] : null; + cond.setAdditionalFamily(parts.length > 1 ? parts[1] : null); } - Page result = fragranceFamilyRepository.searchByFilter(cond, additionalFamily, pageable); // 향수 조회 - return new PageableResponse(result.getContent(), result.getTotalPages(), result.getTotalElements()); - + return cond; } - public PageableResponse searchFragranceListByIngredient(String ingredient, PageRequest pageRequest){ + public PageableResponse searchFragranceByIngredient(String ingredient, PageRequest pageRequest){ Pageable pageable = pageRequest.of(); // pageable 객체로 변환 - Page result = fragranceRepository.searchByIngredient(ingredient, pageable); + Page result = fragranceRepository.searchByIngredient(ingredient, pageable); return new PageableResponse(result.getContent(), result.getTotalPages(), result.getTotalElements()); - } - public DisplayBrand getBrandContent(String brandName){ - // 존재하는지 확인 후 조회 - if(!brandRepository.existsByKorName(brandName)) throw new CustomException(ErrorCode.BRAND_NOT_FOUND); - Brand find = brandRepository.findByKorName(brandName); + public BrandDetailsDto getBrandContent(String brandName){ + Brand find = brandRepository.findByKorName(brandName) + .orElseThrow(() -> new CustomException(ErrorCode.BRAND_NOT_FOUND)); - return DisplayBrand.from(find); + return BrandDetailsDto.from(find); } - public DisplayFamily getFamilyContent(String family) { - // 존재하는지 확인 후 조회 - if(!familyRepository.existsByKorName(family)) throw new CustomException(ErrorCode.FAMILY_NOT_FOUND); - Family find = familyRepository.findByKorName(family); + public FamilyDetailsDto getFamilyContent(String family) { + Family find = familyRepository.findByKorName(family) + .orElseThrow(() -> new CustomException(ErrorCode.FAMILY_NOT_FOUND)); - return DisplayFamily.from(find); + return FamilyDetailsDto.from(find); } - public DisplayIngredient getIngredientContent(String ingredient) { - // 향료가 존재하는지 확인 후 조회 - if(!ingredientRepository.existsByKorName(ingredient)) throw new CustomException(ErrorCode.INGREDIENT_NOT_FOUND); - Ingredient findIngredient = ingredientRepository.findByKorName(ingredient); - - // 향료에 해당하는 향료 타입이 존재하는지 확인 후 조회 - IngredientType findIngredientType = ingredientTypeRepository.findById(findIngredient.getIngredientType().getId()) - .orElseThrow(()-> new CustomException(ErrorCode.INGREDIENT_TYPE_NOT_FOUND)); + public IngredientDetailsDto getIngredientContent(String ingredient) { + Ingredient findIngredient = ingredientRepository.findByKorName(ingredient) + .orElseThrow(() -> new CustomException(ErrorCode.INGREDIENT_NOT_FOUND)); - return DisplayIngredient.from(findIngredient, findIngredientType); + return IngredientDetailsDto.from(findIngredient); } } diff --git a/src/main/java/server/acode/domain/family/service/HomeService.java b/src/main/java/server/acode/domain/family/service/HomeService.java index d602764..cb4be35 100644 --- a/src/main/java/server/acode/domain/family/service/HomeService.java +++ b/src/main/java/server/acode/domain/family/service/HomeService.java @@ -3,7 +3,7 @@ import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import server.acode.domain.family.dto.response.HomeFragrance; +import server.acode.domain.family.dto.response.HomeFragranceDto; import server.acode.domain.family.repository.FamilyRepository; import server.acode.domain.family.repository.FragranceFamilyRepository; import server.acode.domain.ingredient.dto.response.IngredientOfTheDay; @@ -12,9 +12,7 @@ import server.acode.global.exception.CustomException; import java.time.LocalDate; -import java.util.Arrays; import java.util.List; -import java.util.stream.Collectors; @Service @RequiredArgsConstructor @@ -25,19 +23,18 @@ public class HomeService { private final FragranceFamilyRepository fragranceFamilyRepository; private final IngredientRepository ingredientRepository; - public List search(String familyName){ + public List searchMainFragrance(String familyName){ if(!familyRepository.existsByKorName(familyName)) throw new CustomException(ErrorCode.FAMILY_NOT_FOUND); - List result = fragranceFamilyRepository.search(familyName); // 계열과 일치하는 향수 가져오기 + List result = fragranceFamilyRepository.search(familyName); // 계열과 일치하는 향수 가져오기 return result; } public IngredientOfTheDay recommendIngredient() { - List result = ingredientRepository.getTodayIngreient(); + List ingredientOfTheDayGroup = ingredientRepository.getIngredientsOfTheDay(); - int index = LocalDate.now().getDayOfYear() % 5; // 오늘 날짜 기준으로 인덱스 생성 - - return result.get(index); + int order = LocalDate.now().getDayOfYear() % 5; // 오늘 날짜 기준으로 순서 확인 + return ingredientOfTheDayGroup.get(order); // 순서에 해당하는 향료 리턴 } } diff --git a/src/main/java/server/acode/domain/fragrance/dto/Extract.java b/src/main/java/server/acode/domain/fragrance/dto/Extract.java index 0d2e82d..d0b6164 100644 --- a/src/main/java/server/acode/domain/fragrance/dto/Extract.java +++ b/src/main/java/server/acode/domain/fragrance/dto/Extract.java @@ -41,21 +41,19 @@ public List dfs(int idx, List now){ return result; } // AND 결과 있음 } - result.retainAll(relevant(idx)); // sout idx + result.retainAll(relevant(idx)); // 4번째 노드는 AND 포함 결과 없을 때 OR 확인 필요 if(idx == 4 && result.isEmpty()){ result = new ArrayList<>(now); result.retainAll(relevant(5)); } - System.out.println(idx + " : " + "result = "+result); if(!result.isEmpty()) { for (int i = idx + 1; i <= 5; i++) { List newResult = dfs(i, result); - System.out.println(i + " : " + "new result = "+newResult); // 아래 노드에서 다 포함하는 결과가 있다면 그걸로 리턴 if(newResult!=null) { diff --git a/src/main/java/server/acode/domain/fragrance/entity/Fragrance.java b/src/main/java/server/acode/domain/fragrance/entity/Fragrance.java index 7ef8d7f..6f1e98b 100644 --- a/src/main/java/server/acode/domain/fragrance/entity/Fragrance.java +++ b/src/main/java/server/acode/domain/fragrance/entity/Fragrance.java @@ -53,4 +53,5 @@ public class Fragrance extends BaseTimeEntity { private String style; // 스타일 private String season; // 계절감 private String scent; // 향 + } diff --git a/src/main/java/server/acode/domain/fragrance/repository/BrandRepository.java b/src/main/java/server/acode/domain/fragrance/repository/BrandRepository.java index 428d6df..377e401 100644 --- a/src/main/java/server/acode/domain/fragrance/repository/BrandRepository.java +++ b/src/main/java/server/acode/domain/fragrance/repository/BrandRepository.java @@ -4,9 +4,11 @@ import org.springframework.stereotype.Repository; import server.acode.domain.fragrance.entity.Brand; +import java.util.Optional; + @Repository public interface BrandRepository extends JpaRepository, BrandRepositoryCustom { - public Brand findByKorName(String korName); + Optional findByKorName(String korName); boolean existsByKorName(String brandName); } diff --git a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepository.java b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepository.java index a21b623..dd63161 100644 --- a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepository.java +++ b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepository.java @@ -10,6 +10,11 @@ @Repository public interface FragranceRepository extends JpaRepository, FragranceRepositoryCustom { @Modifying + @Query("UPDATE Fragrance f SET f.reviewCnt = f.reviewCnt + :cnt, f.rateSum = f.rateSum + :rate WHERE f.id = :fragranceId") + void updateFragranceForReview(@Param("fragranceId") Long fragranceId, + @Param("rate") int rate, + @Param("cnt") int cnt); + @Modifying @Query("UPDATE Fragrance f SET f.view = f.view + 1 WHERE f.id = :fragranceId") void updateFragranceView(@Param("fragranceId") Long fragranceId); diff --git a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryCustom.java b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryCustom.java index 5e32882..cb0bbe6 100644 --- a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryCustom.java +++ b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryCustom.java @@ -3,14 +3,16 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import server.acode.domain.family.dto.response.DisplayFragrance; +import server.acode.domain.family.dto.response.FragranceCatalogDto; import java.util.List; @Repository public interface FragranceRepositoryCustom { - Page searchByIngredient(String ingredientName, Pageable pageable); + void findWithPessimisticLockById(Long fragranceId); + + Page searchByIngredient(String ingredientName, Pageable pageable); List extractByConcentration(String concentration1, String concentration2); diff --git a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryImpl.java b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryImpl.java index f908976..d59f19c 100644 --- a/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryImpl.java +++ b/src/main/java/server/acode/domain/fragrance/repository/FragranceRepositoryImpl.java @@ -2,14 +2,16 @@ import com.querydsl.core.QueryResults; import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQuery; import com.querydsl.jpa.impl.JPAQueryFactory; import jakarta.persistence.EntityManager; +import jakarta.persistence.LockModeType; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import server.acode.domain.family.dto.response.DisplayFragrance; -import server.acode.domain.family.dto.response.QDisplayFragrance; +import server.acode.domain.family.dto.response.FragranceCatalogDto; +import server.acode.domain.family.dto.response.QFragranceCatalogDto; import server.acode.domain.fragrance.entity.Concentration; import java.util.List; @@ -31,10 +33,22 @@ public FragranceRepositoryImpl(EntityManager em) { } @Override - public Page searchByIngredient(String ingredientName, Pageable pageable) { + public void findWithPessimisticLockById(Long fragranceId) { + queryFactory.select( + fragrance.rateSum, + fragrance.reviewCnt + ) + .from(fragrance) + .where(fragrance.id.eq(fragranceId)) + .setLockMode(LockModeType.PESSIMISTIC_WRITE) + .fetchOne(); + } + + @Override + public Page searchByIngredient(String ingredientName, Pageable pageable) { - QueryResults results = queryFactory - .select(new QDisplayFragrance( + QueryResults results = queryFactory + .select(new QFragranceCatalogDto( fragrance.id.as("fragranceId"), brand.korName.as("brandName"), fragrance.name.as("fragranceName"), @@ -54,7 +68,7 @@ public Page searchByIngredient(String ingredientName, Pageable .groupBy(fragrance.id) .fetchResults(); - List content = results.getResults(); + List content = results.getResults(); long total = results.getTotal(); return new PageImpl<>(content, pageable, total); diff --git a/src/main/java/server/acode/domain/fragrance/service/ExtractService.java b/src/main/java/server/acode/domain/fragrance/service/ExtractService.java index a2abe69..8cb38fd 100644 --- a/src/main/java/server/acode/domain/fragrance/service/ExtractService.java +++ b/src/main/java/server/acode/domain/fragrance/service/ExtractService.java @@ -31,25 +31,15 @@ public class ExtractService { private final FamilyRepository familyRepository; public ExtractResponse extractFamily(KeywordCond cond) { - List fragranceIdList = extractFragranceIdList(cond); - System.out.println("\nfragranceIdList = " + fragranceIdList + "\n"); - + List fragranceIdList = extractFragranceIdListV2(cond); List familyCountDtoList = fragranceFamilyRepository.countFamily(fragranceIdList); - for (FamilyCountDto familyCountDto : familyCountDtoList) { - System.out.println("familyCountDto = " + familyCountDto); - } List familyIdList = familyCountDtoList.stream() .filter(familyCountDto -> familyCountDto.getCount().equals(familyCountDtoList.get(0).getCount())) .map(FamilyCountDto::getFamilyId) .toList(); - System.out.println("\n"); - for (Long l : familyIdList) { - System.out.println("familyId = " + l); - } - - // 계열이 세 개 이상 + // 계열이 세 개 이상인 경우 세 개만 랜덤 선택 if (familyIdList.size() > 3) { Random random = new Random(); List customFamilyIdList = random.ints(0, familyIdList.size()) @@ -59,17 +49,15 @@ public ExtractResponse extractFamily(KeywordCond cond) { .collect(Collectors.toList()); return getExtractResult(customFamilyIdList); - } return getExtractResult(familyIdList); - } /** * 꼼꼼로직 반영한 새 로직 */ - public List newExtractFragranceIdList(KeywordCond cond) { + public List extractFragranceIdListV2(KeywordCond cond) { /* concentration */ List one; if(cond.getConcentration().size() == 2){ @@ -104,34 +92,9 @@ public List newExtractFragranceIdList(KeywordCond cond) { Extract extract = new Extract(one, two, three, fourAnd, fourOr, fiveAnd, fiveOr); return extract.dfs(1, fragranceRepository.extractByConcentration(null, null)); // 모든 향수 id를 list에 넣어서 시작 - - } - - private ExtractResponse getExtractResult (List familyIdList){ - List families = familyRepository.findByIdIn(familyIdList); - List extractFamilies = families.stream() - .map(ExtractFamily::from) - .collect(Collectors.toList()); - - List fragrances = fragranceFamilyRepository.extractFragrance(familyIdList); - - // 향수가 세 개 이상인 경우 - if (fragrances.size() > 5) { - Random random = new Random(); - List collect = random.ints(0, fragrances.size()) - .distinct() - .limit(5) - .mapToObj(fragrances::get) - .collect(Collectors.toList()); - - return new ExtractResponse(extractFamilies, collect); - } - - return new ExtractResponse(extractFamilies, fragrances); } - private List extractFragranceIdList(KeywordCond cond) { List fragranceIdList1; List fragranceIdList2; @@ -259,4 +222,29 @@ private List extractFragranceIdList(KeywordCond cond) { System.out.println("\nRETURN Q5"); return fragranceIdList1; } + + + + private ExtractResponse getExtractResult (List familyIdList){ + List families = familyRepository.findByIdIn(familyIdList); + List extractFamilies = families.stream() + .map(ExtractFamily::from) + .collect(Collectors.toList()); + + List fragrances = fragranceFamilyRepository.extractFragrance(familyIdList); + + // 향수가 다섯 개 이상인 경우 + if (fragrances.size() > 5) { + Random random = new Random(); + List collect = random.ints(0, fragrances.size()) + .distinct() + .limit(5) + .mapToObj(fragrances::get) + .collect(Collectors.toList()); + + return new ExtractResponse(extractFamilies, collect); + } + + return new ExtractResponse(extractFamilies, fragrances); + } } diff --git a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepository.java b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepository.java index 441ffc7..f7a7212 100644 --- a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepository.java +++ b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepository.java @@ -4,10 +4,12 @@ import org.springframework.stereotype.Repository; import server.acode.domain.ingredient.entity.Ingredient; +import java.util.Optional; + @Repository public interface IngredientRepository extends JpaRepository, IngredientRepositoryCustom { - Ingredient findByKorName(String korName); + Optional findByKorName(String korName); boolean existsByKorName(String ingredient); } diff --git a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryCustom.java b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryCustom.java index 9c462d3..b86b339 100644 --- a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryCustom.java +++ b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryCustom.java @@ -7,5 +7,5 @@ @Repository public interface IngredientRepositoryCustom { - List getTodayIngreient(); + List getIngredientsOfTheDay(); } diff --git a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryImpl.java b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryImpl.java index dcfbc3f..686cdbb 100644 --- a/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryImpl.java +++ b/src/main/java/server/acode/domain/ingredient/repository/IngredientRepositoryImpl.java @@ -18,7 +18,7 @@ public class IngredientRepositoryImpl implements IngredientRepositoryCustom{ public IngredientRepositoryImpl(EntityManager em) { this.queryFactory = new JPAQueryFactory(em); } @Override - public List getTodayIngreient() { + public List getIngredientsOfTheDay() { return queryFactory .select(new QIngredientOfTheDay( ingredient.korName.as("ingredientName"), diff --git a/src/main/java/server/acode/domain/review/controller/ReviewController.java b/src/main/java/server/acode/domain/review/controller/ReviewController.java index f880306..c4c1687 100644 --- a/src/main/java/server/acode/domain/review/controller/ReviewController.java +++ b/src/main/java/server/acode/domain/review/controller/ReviewController.java @@ -50,14 +50,14 @@ public ResponseEntity registerReview( @DeleteMapping("/review/{reviewId}") public void deleteReview(@PathVariable Long reviewId){ Long userId= SecurityUtil.getCurrentUserId(); - reviewService.deleteReview(reviewId, userId); + reviewService.deleteCustomerReview(reviewId, userId); } @Operation(summary = "리뷰 삭제/관리자용") @DeleteMapping("/reviewDeveloper/{reviewId}") public void deleteReviewDeveloper(@PathVariable Long reviewId){ Long userId= SecurityUtil.getCurrentUserId(); - reviewService.deleteReviewDeveloper(reviewId, userId); + reviewService.deleteAdminReview(reviewId, userId); } @Operation(summary = "관리자용입니다") diff --git a/src/main/java/server/acode/domain/review/entity/ReviewIntensity.java b/src/main/java/server/acode/domain/review/entity/ReviewIntensity.java index deb9952..79a3c29 100644 --- a/src/main/java/server/acode/domain/review/entity/ReviewIntensity.java +++ b/src/main/java/server/acode/domain/review/entity/ReviewIntensity.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.jpa.repository.Lock; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.global.common.BaseTimeEntity; @@ -28,8 +29,28 @@ public class ReviewIntensity extends BaseTimeEntity { @Column(columnDefinition = "integer default 0") private int intense; + @Version + private Long version; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fragrance_id") private Fragrance fragrance; + public void updateVariable(String intensity, int value){ + switch (intensity){ + case "weak": + weak += value; + break; + case "medium": + medium += value; + break; + case "strong": + strong += value; + break; + case "intense": + intense += value; + break; + } + } + } diff --git a/src/main/java/server/acode/domain/review/entity/ReviewLongevity.java b/src/main/java/server/acode/domain/review/entity/ReviewLongevity.java index d834d75..6ef99ff 100644 --- a/src/main/java/server/acode/domain/review/entity/ReviewLongevity.java +++ b/src/main/java/server/acode/domain/review/entity/ReviewLongevity.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.jpa.repository.Lock; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.global.common.BaseTimeEntity; @@ -28,8 +29,28 @@ public class ReviewLongevity extends BaseTimeEntity { @Column(columnDefinition = "integer default 0") private int fullday; + @Version + private Long version; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fragrance_id") private Fragrance fragrance; + public void updateVariable(String longevity, int value){ + switch (longevity){ + case "onehour": + onehour += value; + break; + case "fourhours": + fourhours += value; + break; + case "halfday": + halfday += value; + break; + case "fullday": + fullday += value; + break; + } + } + } diff --git a/src/main/java/server/acode/domain/review/entity/ReviewSeason.java b/src/main/java/server/acode/domain/review/entity/ReviewSeason.java index 7f06a8e..60049f3 100644 --- a/src/main/java/server/acode/domain/review/entity/ReviewSeason.java +++ b/src/main/java/server/acode/domain/review/entity/ReviewSeason.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.jpa.repository.Lock; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.global.common.BaseTimeEntity; @@ -31,7 +32,30 @@ public class ReviewSeason extends BaseTimeEntity { @Column(columnDefinition = "integer default 0") private int allSeasons; + @Version + private Long version; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fragrance_id") private Fragrance fragrance; + + public void updateVariable(String season, int value){ + switch (season){ + case "spring": + spring += value; + break; + case "summer": + summer += value; + break; + case "autumn": + autumn += value; + break; + case "winter": + winter += value; + break; + case "allseasons": + allSeasons += value; + break; + } + } } diff --git a/src/main/java/server/acode/domain/review/entity/ReviewStyle.java b/src/main/java/server/acode/domain/review/entity/ReviewStyle.java index d1fb8b3..e83e585 100644 --- a/src/main/java/server/acode/domain/review/entity/ReviewStyle.java +++ b/src/main/java/server/acode/domain/review/entity/ReviewStyle.java @@ -4,6 +4,7 @@ import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; +import org.springframework.data.jpa.repository.Lock; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.global.common.BaseTimeEntity; @@ -82,7 +83,85 @@ public class ReviewStyle extends BaseTimeEntity { @Column(columnDefinition = "integer default 0") private int cozy; //포근한 + @Version + private Long version; + @OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "fragrance_id") private Fragrance fragrance; + + public void updateVariable(String[] styles, int value){ + // TODO 확장성 높이기 + for (int i = 0; i < styles.length; i++) { + switch (styles[i]){ + case "chic": + chic += value; + break; + case "mature": + mature += value; + break; + case "luxurious": + luxurious += value; + break; + case "elegant": + elegant += value; + break; + case "masculine": + masculine += value; + break; + case "comfortable": + comfortable += value; + break; + case "serene": + serene += value; + break; + case "light": + light += value; + break; + case "neutral": + neutral += value; + break; + case "friendly": + friendly += value; + break; + case "clean": + clean += value; + break; + case "sensual": + sensual += value; + break; + case "delicate": + delicate += value; + break; + case "lively": + lively += value; + break; + case "lovely": + lovely += value; + break; + case "bright": + bright += value; + break; + case "radiant": + radiant += value; + break; + case "feminine": + feminine += value; + break; + case "innocent": + innocent += value; + break; + case "weighty": + weighty += value; + break; + case "soft": + soft += value; + break; + case "cozy": + cozy += value; + break; + } + + } + } } diff --git a/src/main/java/server/acode/domain/review/repository/ReviewRepositoryCustom.java b/src/main/java/server/acode/domain/review/repository/ReviewRepositoryCustom.java index db169a2..852f9bc 100644 --- a/src/main/java/server/acode/domain/review/repository/ReviewRepositoryCustom.java +++ b/src/main/java/server/acode/domain/review/repository/ReviewRepositoryCustom.java @@ -6,7 +6,7 @@ import server.acode.domain.fragrance.dto.response.ReviewInfo; import server.acode.domain.fragrance.dto.response.ReviewPreview; import server.acode.domain.fragrance.entity.Fragrance; -import server.acode.domain.user.dto.response.DisplayReview; +import server.acode.domain.user.dto.response.ReviewDto; import java.util.List; @@ -16,5 +16,5 @@ public interface ReviewRepositoryCustom { Page getReviewInfoPage(Long fragranceId, Pageable pageable); - Page getDisplayReview(Long userId, Pageable pageable); + Page getDisplayReview(Long userId, Pageable pageable); } diff --git a/src/main/java/server/acode/domain/review/repository/ReviewRepositoryImpl.java b/src/main/java/server/acode/domain/review/repository/ReviewRepositoryImpl.java index b02e132..dde801f 100644 --- a/src/main/java/server/acode/domain/review/repository/ReviewRepositoryImpl.java +++ b/src/main/java/server/acode/domain/review/repository/ReviewRepositoryImpl.java @@ -14,8 +14,8 @@ import server.acode.domain.fragrance.dto.response.ReviewInfo; import server.acode.domain.fragrance.dto.response.ReviewPreview; import server.acode.domain.fragrance.entity.Fragrance; -import server.acode.domain.user.dto.response.DisplayReview; -import server.acode.domain.user.dto.response.QDisplayReview; +import server.acode.domain.user.dto.response.QReviewDto; +import server.acode.domain.user.dto.response.ReviewDto; import java.util.List; @@ -84,9 +84,9 @@ public Page getReviewInfoPage(Long fragranceId, Pageable pageable) { } @Override - public Page getDisplayReview(Long userId, Pageable pageable){ - QueryResults results = queryFactory - .select(new QDisplayReview( + public Page getDisplayReview(Long userId, Pageable pageable){ + QueryResults results = queryFactory + .select(new QReviewDto( review.id, review.fragrance.id, review.fragrance.name, @@ -103,7 +103,7 @@ public Page getDisplayReview(Long userId, Pageable pageable){ .limit(pageable.getPageSize()) .fetchResults(); - List content = results.getResults(); + List content = results.getResults(); long total = results.getTotal(); return new PageImpl<>(content, pageable, total); diff --git a/src/main/java/server/acode/domain/review/repository/ReviewUpdateRepository.java b/src/main/java/server/acode/domain/review/repository/ReviewUpdateRepository.java deleted file mode 100644 index c03680f..0000000 --- a/src/main/java/server/acode/domain/review/repository/ReviewUpdateRepository.java +++ /dev/null @@ -1,64 +0,0 @@ -package server.acode.domain.review.repository; - -import com.querydsl.core.types.dsl.Expressions; -import com.querydsl.core.types.dsl.NumberPath; -import com.querydsl.jpa.impl.JPAQueryFactory; -import jakarta.persistence.EntityManager; -import org.springframework.stereotype.Repository; - -import static server.acode.domain.review.entity.QReviewIntensity.reviewIntensity; -import static server.acode.domain.review.entity.QReviewLongevity.reviewLongevity; -import static server.acode.domain.review.entity.QReviewSeason.reviewSeason; -import static server.acode.domain.review.entity.QReviewStyle.reviewStyle; - -@Repository -public class ReviewUpdateRepository { - private final JPAQueryFactory queryFactory; - - public ReviewUpdateRepository(EntityManager em) { - this.queryFactory = new JPAQueryFactory(em); - } - - public void updateSeason(String season, long fragranceId, int value){ - NumberPath dynamicPath = Expressions.numberPath(int.class, reviewSeason, season); - - // 업데이트 쿼리 작성 - queryFactory.update(reviewSeason) - .where(reviewSeason.fragrance.id.eq(fragranceId)) - .set(dynamicPath, dynamicPath.add(value)) - .execute(); - - } - - public void updateLongevity(String longevity, long fragranceId, int value){ - NumberPath dynamicPath = Expressions.numberPath(int.class, reviewLongevity, longevity); - - // 업데이트 쿼리 작성 - queryFactory.update(reviewLongevity) - .where(reviewLongevity.fragrance.id.eq(fragranceId)) - .set(dynamicPath, dynamicPath.add(value)) - .execute(); - } - - public void updateIntensity(String intensity, long fragranceId, int value){ - NumberPath dynamicPath = Expressions.numberPath(int.class, reviewIntensity, intensity); - - // 업데이트 쿼리 작성 - queryFactory.update(reviewIntensity) - .where(reviewIntensity.fragrance.id.eq(fragranceId)) - .set(dynamicPath, dynamicPath.add(value)) - .execute(); - } - - public void updateStyle(String style, long fragranceId, int value){ - NumberPath dynamicPath = Expressions.numberPath(int.class, reviewStyle, style); - - // 업데이트 쿼리 작성 - queryFactory.update(reviewStyle) - .where(reviewStyle.fragrance.id.eq(fragranceId)) - .set(dynamicPath, dynamicPath.add(value)) - .execute(); - } - - -} diff --git a/src/main/java/server/acode/domain/review/service/ReviewService.java b/src/main/java/server/acode/domain/review/service/ReviewService.java index 8ceda44..43eb6f3 100644 --- a/src/main/java/server/acode/domain/review/service/ReviewService.java +++ b/src/main/java/server/acode/domain/review/service/ReviewService.java @@ -1,5 +1,6 @@ package server.acode.domain.review.service; +import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; @@ -32,7 +33,7 @@ public class ReviewService { private final ReviewLongevityRepository reviewLongevityRepository; private final ReviewIntensityRepository reviewIntensityRepository; private final ReviewStyleRepository reviewStyleRepository; - private final ReviewUpdateRepository reviewUpdateRepository; + private final EntityManager em; public ResponseEntity registerReview(Long fragranceId, RegisterReviewRequest registerReviewRequest, Long userId) { User user = userRepository.findById(userId) @@ -186,17 +187,21 @@ public ResponseEntity insertStatistics() { return ResponseEntity.ok().build(); } - public void deleteReview(Long reviewId, Long userId) { + + /** + * 리뷰 삭제 + */ + public void deleteCustomerReview(Long reviewId, Long userId) { Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> new CustomException(ErrorCode.REVIEW_NOT_FOUND)); if (review.getUser().getId() == userId) { - deleteReviewLogic(review); + deleteReview(review); } else { throw new CustomException(ErrorCode.REVIEW_AUTHOR_MISMATCH); } } - public void deleteReviewDeveloper(Long reviewId, Long userId) { + public void deleteAdminReview(Long reviewId, Long userId) { Review review = reviewRepository.findById(reviewId) .orElseThrow(() -> new CustomException(ErrorCode.REVIEW_NOT_FOUND)); @@ -205,37 +210,50 @@ public void deleteReviewDeveloper(Long reviewId, Long userId) { .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); if (user.getRole().equals(Role.ROLE_ADMIN)){ - deleteReviewLogic(review); + deleteReview(review); } else { throw new CustomException(ErrorCode.NOT_ADMIN);} } - private void deleteReviewLogic(Review review){ + private void deleteReview(Review review){ + /** + * 향수의 리뷰 별점 총 합 -= 리뷰 별 점 + * 비관적 락을 위한 업데이트용 find method 호출 + */ + Long frgranceId = review.getFragrance().getId(); + fragranceRepository.findWithPessimisticLockById(frgranceId); + fragranceRepository.updateFragranceForReview(frgranceId, 0-review.getRate(), -1); + + /** * ReviewSeason, ReviewLongevity, ReviewIntensity, ReviewStyle에 해당하는 컬럼 값 -= 1 */ + Fragrance fragrance = fragranceRepository.findById(frgranceId).orElseThrow(); + String season = review.getSeason().toString().toLowerCase(); - reviewUpdateRepository.updateSeason(season, review.getFragrance().getId(), -1); + ReviewSeason reviewSeason = reviewSeasonRepository.findByFragrance(fragrance).get(); + reviewSeason.updateVariable(season, -1); + String longevity = review.getLongevity().toString().toLowerCase(); // 지속성 - reviewUpdateRepository.updateLongevity(longevity, review.getFragrance().getId(), -1); + ReviewLongevity reviewLongevity = reviewLongevityRepository.findByFragrance(fragrance).get(); + reviewLongevity.updateVariable(longevity, -1); - String intensity = review.getIntensity().toString().toLowerCase(); // 향의 세기 - reviewUpdateRepository.updateIntensity(intensity, review.getFragrance().getId(), -1); - String styleList = review.getStyle().toLowerCase(); - List styles = Arrays.asList(styleList.split(", ")); - styles.forEach(style -> reviewUpdateRepository.updateStyle(style, review.getFragrance().getId(), -1)); + String intensity = review.getIntensity().toString().toLowerCase(); // 향의 세기 + ReviewIntensity reviewIntensity = reviewIntensityRepository.findByFragrance(fragrance).get(); + reviewIntensity.updateVariable(intensity, -1); - /** - * Fragrance 테이블의 reviewCnt -= 1 - */ - fragranceRepository.decreaseReview(review.getRate(), review.getFragrance().getId()); + String styleList = review.getStyle().toLowerCase(); + String[] styles = styleList.split(", "); + ReviewStyle reviewStyle = reviewStyleRepository.findByFragrance(fragrance).get(); + reviewStyle.updateVariable(styles, -1); /** - * Review 테이블 삭제 + * review 테이블 hard-delete */ reviewRepository.delete(review); } + } diff --git a/src/main/java/server/acode/domain/user/controller/UserController.java b/src/main/java/server/acode/domain/user/controller/UserController.java index a39d77c..fc946c3 100644 --- a/src/main/java/server/acode/domain/user/controller/UserController.java +++ b/src/main/java/server/acode/domain/user/controller/UserController.java @@ -4,15 +4,11 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; -import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.security.core.context.SecurityContextHolder; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; import server.acode.domain.family.dto.response.PageableResponse; -import server.acode.domain.user.dto.response.PreviewUserInfo; +import server.acode.domain.user.dto.response.UserBasicInfoDto; import server.acode.domain.user.service.UserService; import server.acode.domain.user.dto.request.NicknameRequest; -import server.acode.global.auth.security.CustomUserDetails; import server.acode.global.common.PageRequest; import server.acode.global.util.SecurityUtil; @@ -37,15 +33,15 @@ public void updateNickname(@RequestBody @Valid NicknameRequest request){ description = "중복되는 닉네임이 있다면 409 에러 반환됩니다.") @PostMapping("/users/nickname") public void checkNickname(@RequestBody @Valid NicknameRequest request){ - userService.checkNickname(request.getNickname()); + userService.checkDuplicatedNickname(request.getNickname()); } @Operation(summary = "마이페이지 기본 정보") @GetMapping("/mypage") - public PreviewUserInfo getUserInfo(){ + public UserBasicInfoDto getUserBasicInfo(){ Long userId = SecurityUtil.getCurrentUserId(); - return userService.getUserInfo(userId); + return userService.getUserBasicInfo(userId); } @Operation(summary = "마이페이지 스크랩 리스트업", diff --git a/src/main/java/server/acode/domain/user/dto/response/DisplayReview.java b/src/main/java/server/acode/domain/user/dto/response/ReviewDto.java similarity index 79% rename from src/main/java/server/acode/domain/user/dto/response/DisplayReview.java rename to src/main/java/server/acode/domain/user/dto/response/ReviewDto.java index 9c2dc8b..fe6018d 100644 --- a/src/main/java/server/acode/domain/user/dto/response/DisplayReview.java +++ b/src/main/java/server/acode/domain/user/dto/response/ReviewDto.java @@ -6,7 +6,7 @@ @Getter @NoArgsConstructor -public class DisplayReview { +public class ReviewDto { private Long reviewId; private Long fragranceId; private String fragranceName; @@ -16,7 +16,7 @@ public class DisplayReview { private String thumbnail; @QueryProjection - public DisplayReview(Long reviewId, Long fragranceId, String fragranceName, String brandName, String comment, int rate, String thumbnail){ + public ReviewDto(Long reviewId, Long fragranceId, String fragranceName, String brandName, String comment, int rate, String thumbnail){ this.reviewId = reviewId; this.fragranceId = fragranceId; this.fragranceName = fragranceName; diff --git a/src/main/java/server/acode/domain/user/dto/response/DisplayScrap.java b/src/main/java/server/acode/domain/user/dto/response/ScrapDto.java similarity index 69% rename from src/main/java/server/acode/domain/user/dto/response/DisplayScrap.java rename to src/main/java/server/acode/domain/user/dto/response/ScrapDto.java index 4f39b90..f281db5 100644 --- a/src/main/java/server/acode/domain/user/dto/response/DisplayScrap.java +++ b/src/main/java/server/acode/domain/user/dto/response/ScrapDto.java @@ -7,24 +7,21 @@ @Getter @NoArgsConstructor -public class DisplayScrap { +public class ScrapDto { private Long fragranceId; private String fragranceName; private String brandName; private String concentration; private String thumbnail; -// private String capacity; -// private int price; + @QueryProjection - public DisplayScrap(Long fragranceId, String fragranceName, String brandName, Concentration concentration, String thumbnail){ + public ScrapDto(Long fragranceId, String fragranceName, String brandName, Concentration concentration, String thumbnail){ this.fragranceId = fragranceId; this.fragranceName = fragranceName; this.brandName = brandName; this.concentration = concentration.toString(); this.thumbnail = thumbnail; -// this.capacity = capacity; -// this.price = price; } } diff --git a/src/main/java/server/acode/domain/user/dto/response/PreviewScrap.java b/src/main/java/server/acode/domain/user/dto/response/ScrapPreviewDto.java similarity index 78% rename from src/main/java/server/acode/domain/user/dto/response/PreviewScrap.java rename to src/main/java/server/acode/domain/user/dto/response/ScrapPreviewDto.java index bbf1cde..d962f12 100644 --- a/src/main/java/server/acode/domain/user/dto/response/PreviewScrap.java +++ b/src/main/java/server/acode/domain/user/dto/response/ScrapPreviewDto.java @@ -6,12 +6,12 @@ @Getter @NoArgsConstructor -public class PreviewScrap { +public class ScrapPreviewDto { private Long fragranceId; private String thumbnail; @QueryProjection - public PreviewScrap(Long fragranceId, String thumbnail){ + public ScrapPreviewDto(Long fragranceId, String thumbnail){ this.fragranceId = fragranceId; this.thumbnail = thumbnail; } diff --git a/src/main/java/server/acode/domain/user/dto/response/PreviewUserInfo.java b/src/main/java/server/acode/domain/user/dto/response/UserBasicInfoDto.java similarity index 68% rename from src/main/java/server/acode/domain/user/dto/response/PreviewUserInfo.java rename to src/main/java/server/acode/domain/user/dto/response/UserBasicInfoDto.java index d80174b..9ac7f81 100644 --- a/src/main/java/server/acode/domain/user/dto/response/PreviewUserInfo.java +++ b/src/main/java/server/acode/domain/user/dto/response/UserBasicInfoDto.java @@ -9,13 +9,13 @@ @Getter @NoArgsConstructor -public class PreviewUserInfo { +public class UserBasicInfoDto { private String nickname; private int reviewCnt; - private List scraps; + private List scraps; @Builder - public PreviewUserInfo(String nickname, int reviewCnt, List scraps){ + public UserBasicInfoDto(String nickname, int reviewCnt, List scraps){ this.nickname = nickname; this.reviewCnt = reviewCnt; this.scraps = scraps; diff --git a/src/main/java/server/acode/domain/user/entity/User.java b/src/main/java/server/acode/domain/user/entity/User.java index 48aba82..cad67ab 100644 --- a/src/main/java/server/acode/domain/user/entity/User.java +++ b/src/main/java/server/acode/domain/user/entity/User.java @@ -5,7 +5,6 @@ import lombok.Builder; import lombok.Getter; import lombok.NoArgsConstructor; -import org.hibernate.annotations.Where; import server.acode.global.common.BaseTimeEntity; import java.util.Objects; @@ -47,11 +46,11 @@ public User(Long id, Role role, String authKey, String nickname){ this.isDel = false; } - public void updateNickname(String newOne) { + public void setNickname(String newOne) { this.nickname = newOne; } - public void updateIsDel(boolean delColumn){ + public void setIsDel(boolean delColumn){ this.isDel = delColumn; } } diff --git a/src/main/java/server/acode/domain/user/repository/ScrapRepositoryCustom.java b/src/main/java/server/acode/domain/user/repository/ScrapRepositoryCustom.java index e2bd6b5..1ebadad 100644 --- a/src/main/java/server/acode/domain/user/repository/ScrapRepositoryCustom.java +++ b/src/main/java/server/acode/domain/user/repository/ScrapRepositoryCustom.java @@ -3,15 +3,15 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Repository; -import server.acode.domain.user.dto.response.DisplayScrap; -import server.acode.domain.user.dto.response.PreviewScrap; +import server.acode.domain.user.dto.response.ScrapDto; +import server.acode.domain.user.dto.response.ScrapPreviewDto; import java.util.List; @Repository public interface ScrapRepositoryCustom { - List getScrapPreview(Long userId); + List getScrapPreview(Long userId); - Page getScrap(Long userId, Pageable pageable); + Page getScrap(Long userId, Pageable pageable); } diff --git a/src/main/java/server/acode/domain/user/repository/ScrapRepositoryImpl.java b/src/main/java/server/acode/domain/user/repository/ScrapRepositoryImpl.java index 48d20e8..3fd2ffb 100644 --- a/src/main/java/server/acode/domain/user/repository/ScrapRepositoryImpl.java +++ b/src/main/java/server/acode/domain/user/repository/ScrapRepositoryImpl.java @@ -24,9 +24,9 @@ public ScrapRepositoryImpl(EntityManager em) { } @Override - public List getScrapPreview(Long userId){ + public List getScrapPreview(Long userId){ return queryFactory - .select(new QPreviewScrap( + .select(new QScrapPreviewDto( fragrance.id, fragrance.thumbnail )) @@ -40,9 +40,9 @@ public List getScrapPreview(Long userId){ } @Override - public Page getScrap(Long userId, Pageable pageable){ - QueryResults results = queryFactory - .select(new QDisplayScrap( + public Page getScrap(Long userId, Pageable pageable){ + QueryResults results = queryFactory + .select(new QScrapDto( scrap.fragrance.id.as("fragranceId"), scrap.fragrance.name.as("fragranceName"), scrap.fragrance.brand.korName.as("brandName"), @@ -57,7 +57,7 @@ public Page getScrap(Long userId, Pageable pageable){ .limit(pageable.getPageSize()) .fetchResults(); - List content = results.getResults(); + List content = results.getResults(); long total = results.getTotal(); return new PageImpl<>(content, pageable, total); diff --git a/src/main/java/server/acode/domain/user/service/UserService.java b/src/main/java/server/acode/domain/user/service/UserService.java index ac997ca..b4a3d5c 100644 --- a/src/main/java/server/acode/domain/user/service/UserService.java +++ b/src/main/java/server/acode/domain/user/service/UserService.java @@ -7,10 +7,10 @@ import org.springframework.transaction.annotation.Transactional; import server.acode.domain.family.dto.response.PageableResponse; import server.acode.domain.review.repository.ReviewRepository; -import server.acode.domain.user.dto.response.DisplayReview; -import server.acode.domain.user.dto.response.DisplayScrap; -import server.acode.domain.user.dto.response.PreviewScrap; -import server.acode.domain.user.dto.response.PreviewUserInfo; +import server.acode.domain.user.dto.response.ReviewDto; +import server.acode.domain.user.dto.response.ScrapDto; +import server.acode.domain.user.dto.response.ScrapPreviewDto; +import server.acode.domain.user.dto.response.UserBasicInfoDto; import server.acode.domain.user.entity.User; import server.acode.domain.user.repository.ScrapRepository; import server.acode.domain.user.repository.UserRepository; @@ -34,44 +34,44 @@ public synchronized void synchronizedUpdateNickname(String nickname, Long userId @Transactional public void updateNickname(String nickname, Long userId) { - checkNickname(nickname); + checkDuplicatedNickname(nickname); User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - user.updateNickname(nickname); + user.setNickname(nickname); userRepository.save(user); } @Transactional(readOnly = true) - public void checkNickname(String nickname) { + public void checkDuplicatedNickname(String nickname) { userRepository.findByNicknameAndIsDel(nickname, false).ifPresent(user -> { throw new CustomException(ErrorCode.NICKNAME_ALREADY_USED); }); } @Transactional(readOnly = true) - public PreviewUserInfo getUserInfo(Long userId){ + public UserBasicInfoDto getUserBasicInfo(Long userId){ User user = userRepository.findById(userId) .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - List previewScrap = scrapRepository.getScrapPreview(userId); + List scrapPreview = scrapRepository.getScrapPreview(userId); - PreviewUserInfo info = PreviewUserInfo.builder() + UserBasicInfoDto response = UserBasicInfoDto.builder() .nickname(user.getNickname()) .reviewCnt(reviewRepository.countByUserId(userId)) - .scraps(previewScrap) + .scraps(scrapPreview) .build(); - return info; + return response; } @Transactional(readOnly = true) public PageableResponse getScrapList(Long userId, PageRequest pageRequest){ Pageable pageable = pageRequest.of(); // pageable 객체로 변환 - Page result = scrapRepository.getScrap(userId, pageable); + Page result = scrapRepository.getScrap(userId, pageable); return new PageableResponse(result.getContent(), result.getTotalPages(), result.getTotalElements()); } @@ -79,9 +79,8 @@ public PageableResponse getScrapList(Long userId, PageRequest pageRequest){ public PageableResponse getReviewList(Long userId, PageRequest pageRequest) { Pageable pageable = pageRequest.of(); // pageable 객체로 변환 - Page result = reviewRepository.getDisplayReview(userId, pageable); + Page result = reviewRepository.getDisplayReview(userId, pageable); return new PageableResponse(result.getContent(), result.getTotalPages(), result.getTotalElements()); } - -} +} \ No newline at end of file diff --git a/src/main/java/server/acode/global/auth/AuthService.java b/src/main/java/server/acode/global/auth/AuthService.java index 013976d..c3e58b5 100644 --- a/src/main/java/server/acode/global/auth/AuthService.java +++ b/src/main/java/server/acode/global/auth/AuthService.java @@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.*; import org.springframework.stereotype.Service; @@ -22,6 +23,7 @@ @Service @RequiredArgsConstructor @Transactional +@Slf4j public class AuthService { private final RedisDao redisDao; @@ -37,30 +39,10 @@ public class AuthService { @Value("${KAKAO_REDIRECT_URL}") private String redirectedUrl; - public void checkUser(Long userId){ - /** - * 사용자 정보 이용하는 부분 모두 - * Long userId = SecurityUtil.getCurrentUserId(); - * - * User user = userRepository.findById(userId) - * .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - * 으로 수정해야함 - */ - User user = userRepository.findById(userId) - .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND)); - - System.out.println("user = " + user.getNickname()); - } - - public ResponseEntity signin(String code, boolean developer) throws JsonProcessingException { + public ResponseEntity signin(String code, boolean developer) { String kakaoAccessToken = getKakaoAccessToken(code, developer); - String userInfo = getUserInfo(kakaoAccessToken); - - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode jsonNode = objectMapper.readTree(userInfo); - - // id 값을 추출 - String authKey = jsonNode.get("id").toString(); + String userInfo = getKakaoInfo(kakaoAccessToken); + String authKey = sendParseValue(userInfo, "id"); HttpStatus init = HttpStatus.OK; if(!userRepository.existsByAuthKeyAndIsDel(authKey, false)) { @@ -76,8 +58,7 @@ public ResponseEntity signin(String code, boolean developer) throws JsonProcessi return new ResponseEntity<>(token, init); } - // TODO Redirect url 숨기기 - private String getKakaoAccessToken(String code, boolean developer) throws JsonProcessingException { + private String getKakaoAccessToken(String code, boolean developer){ // header 생성 HttpHeaders headers = new HttpHeaders(); headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); @@ -93,32 +74,31 @@ private String getKakaoAccessToken(String code, boolean developer) throws JsonPr } else { params.add("redirect_uri", redirectedUrl); // 배포용 redirect uri } - System.out.println(code); // header + body HttpEntity> httpEntity = new HttpEntity<>(params, headers); // http 요청하기 RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.exchange( - "https://kauth.kakao.com/oauth/token", - HttpMethod.POST, - httpEntity, - String.class - ); - - ObjectMapper objectMapper = new ObjectMapper(); - JsonNode jsonNode = objectMapper.readTree(response.getBody()); - - // access_token의 값을 읽어오기 - String accessToken = jsonNode.get("access_token").asText(); + ResponseEntity response; + try { + response = restTemplate.exchange( + "https://kauth.kakao.com/oauth/token", + HttpMethod.POST, + httpEntity, + String.class + ); + } catch (Exception e){ + log.error("사용자의 카카오 로그인 인증 코드가 유효하지 않습니다.: " + e.toString()); + throw new CustomException(ErrorCode.INVALID_AUTHENTICATION_CODE); + } - System.out.println("Access Token: " + accessToken); + String accessToken = sendParseValue(response.getBody(), "access_token"); + log.info("Access Token: " + accessToken); return accessToken; - } - private String getUserInfo(String accessToken) { + private String getKakaoInfo(String accessToken) { // header 생성 HttpHeaders headers = new HttpHeaders(); headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); @@ -136,6 +116,25 @@ private String getUserInfo(String accessToken) { return response.getBody(); } + private String sendParseValue(String jsonInfo, String parsingKey){ + try { + return tryToParseValue(jsonInfo, parsingKey); + } catch (JsonProcessingException e) { + log.error("카카오 응답 과정 중 json parsing error: " + e.toString()); + throw new CustomException(ErrorCode.JSON_PARSING_ERROR); + } + } + + private String tryToParseValue(String jsonInfo, String parsingKey) throws JsonProcessingException { + ObjectMapper objectMapper = new ObjectMapper(); + JsonNode jsonNode = null; + jsonNode = objectMapper.readTree(jsonInfo); // 예외 발생 지점 + + // id 값을 추출 + String parsingValue = jsonNode.get(parsingKey).toString(); + return parsingValue; + } + private User createUser(String authKey){ User user = User.builder() .authKey(authKey) @@ -155,7 +154,7 @@ public ResponseEntity withdrawal(Long userId) { // soft-delete - currentUser.updateIsDel(true); + currentUser.setIsDel(true); userRepository.save(currentUser); /** 카카오 unlink 처리 **/ diff --git a/src/main/java/server/acode/global/common/ErrorCode.java b/src/main/java/server/acode/global/common/ErrorCode.java index e690290..36d166c 100644 --- a/src/main/java/server/acode/global/common/ErrorCode.java +++ b/src/main/java/server/acode/global/common/ErrorCode.java @@ -18,9 +18,11 @@ public enum ErrorCode { BRAND_NOT_FOUND(HttpStatus.BAD_REQUEST,"해당 브랜드를 찾을 수 없습니다." ), SEARCH_NOT_FOUND(HttpStatus.BAD_REQUEST, "검색어가 필요합니다"), REVIEW_NOT_FOUND(HttpStatus.BAD_REQUEST, "해당 리뷰를 찾을 수 없습니다."), + INVALID_AUTHENTICATION_CODE(HttpStatus.BAD_REQUEST, "유효한 인증 코드가 아닙니다."), /* 401 UNAUTHORIZED */ + EXPIRED_TOKEN_EXCEPTION(HttpStatus.UNAUTHORIZED, "만료된 토큰입니다."), TOKEN_VALIDATION_EXCEPTION(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."), @@ -40,12 +42,12 @@ public enum ErrorCode { /* 409 CONFLICT */ UNLINK_FAIL(HttpStatus.CONFLICT, "UNLINK 도중 실패했습니다."), NICKNAME_ALREADY_USED(HttpStatus.CONFLICT, "이미 존재하는 닉네임입니다."), - REVIEW_AUTHOR_MISMATCH(HttpStatus.CONFLICT, "리뷰 작성자와 일치하지 않는 사용자입니다."); + REVIEW_AUTHOR_MISMATCH(HttpStatus.CONFLICT, "리뷰 작성자와 일치하지 않는 사용자입니다."), // Review author mismatch /* 500 INTERNAL SERVER ERROR */ -// INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "관리자에게 문의 바랍니다."); + JSON_PARSING_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "JSON parsing 에러입니다."); private final HttpStatus httpStatus; private final String message; diff --git a/src/test/java/server/acode/domain/review/repository/ReviewUpdateRepositoryTest.java b/src/test/java/server/acode/domain/review/repository/ReviewUpdateRepositoryTest.java deleted file mode 100644 index d2e6c1c..0000000 --- a/src/test/java/server/acode/domain/review/repository/ReviewUpdateRepositoryTest.java +++ /dev/null @@ -1,40 +0,0 @@ -package server.acode.domain.review.repository; - -import jakarta.persistence.EntityManager; -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; -import server.acode.domain.fragrance.entity.Fragrance; -import server.acode.domain.review.entity.ReviewSeason; - -import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; -@SpringBootTest -@Transactional -class ReviewUpdateRepositoryTest { - @Autowired - EntityManager em; - @Autowired ReviewUpdateRepository reviewUpdateRepository; - @Autowired ReviewSeasonRepository reviewSeasonRepository; - - @Test - public void updateTest(){ - // given - Fragrance testFragrance1 = Fragrance.builder() - .name("testFragrance1") - .build(); - em.persist(testFragrance1); - reviewSeasonRepository.insertStatistics(testFragrance1.getId()); // ReviewSeason 생성 - - // when - reviewUpdateRepository.updateSeason("spring", testFragrance1.getId(), 1); - em.flush(); - em.clear(); - - // then - Optional reviewSeason = reviewSeasonRepository.findByFragrance(testFragrance1); - assertThat(reviewSeason.get().getSpring()).isEqualTo(1); - } -} \ No newline at end of file diff --git a/src/test/java/server/acode/domain/review/service/ReviewServiceTest.java b/src/test/java/server/acode/domain/review/service/ReviewServiceTest.java index 18e3bd5..554f55d 100644 --- a/src/test/java/server/acode/domain/review/service/ReviewServiceTest.java +++ b/src/test/java/server/acode/domain/review/service/ReviewServiceTest.java @@ -4,25 +4,26 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.domain.fragrance.repository.FragranceRepository; -import server.acode.domain.review.dto.request.RegisterReviewRequest; -import server.acode.domain.user.entity.User; -import server.acode.global.exception.CustomException; +import server.acode.domain.review.entity.ReviewSeason; +import server.acode.domain.review.repository.ReviewSeasonRepository; -import java.util.Optional; -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; +import java.util.concurrent.*; import static org.assertj.core.api.Assertions.*; + @SpringBootTest @Transactional +@ActiveProfiles("test") class ReviewServiceTest { @Autowired ReviewService reviewService; @Autowired FragranceRepository fragranceRepository; + @Autowired ReviewSeasonRepository reviewSeasonRepository; @DisplayName("리뷰 작성/ 삭제가 여러 스레드에서 일어날 때 reviewCnt 일관성 유지 테스트") @Test @@ -36,22 +37,42 @@ void reviewTest() throws InterruptedException { //when service.execute(() -> { - RegisterReviewRequest request = new RegisterReviewRequest(3, "very good", "SPRING", "ONEHOUR", "WEAK", "CHIC"); - reviewService.registerReview(1L, request,1L); - latch.countDown(); + try { + reviewService.deleteCustomerReview(1L, 1L); + } catch (ObjectOptimisticLockingFailureException e) { + } finally { + latch.countDown(); + } }); service.execute(() -> { - reviewService.deleteReview(3L, 1L); - latch.countDown(); - }); + try { + reviewService.deleteCustomerReview(2L, 2L); + } catch (ObjectOptimisticLockingFailureException e) { + } finally { + latch.countDown(); + } + }); latch.await(); // 모든 쓰레드의 작업이 완료될 때까지 대기 //then Fragrance fragrance = fragranceRepository.findById(1L).get(); - assertThat(fragrance.getReviewCnt()).isEqualTo(1); + ReviewSeason season = reviewSeasonRepository.findByFragrance(fragrance).get(); + + if(season.getVersion() == 1){ + // 하나의 스레드가 락에 걸린 경우 + assertThat(fragrance.getReviewCnt()).isEqualTo(1); + assertThat(season.getSpring()).isEqualTo(1); + } else if(season.getVersion() == 2) { + // 두 스레드 모두 정상적으로 실행된 경우 + assertThat(fragrance.getReviewCnt()).isEqualTo(0); + assertThat(fragrance.getRateSum()).isEqualTo(0); + assertThat(season.getSpring()).isEqualTo(0); + } else { + fail("Unexpected version: " + season.getVersion()); + } } } \ No newline at end of file diff --git a/src/test/java/server/acode/domain/user/repository/ScrapRepositoryImplTest.java b/src/test/java/server/acode/domain/user/repository/ScrapRepositoryImplTest.java index edb35db..3e3768d 100644 --- a/src/test/java/server/acode/domain/user/repository/ScrapRepositoryImplTest.java +++ b/src/test/java/server/acode/domain/user/repository/ScrapRepositoryImplTest.java @@ -6,12 +6,13 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; import server.acode.domain.fragrance.entity.Brand; import server.acode.domain.fragrance.entity.Concentration; import server.acode.domain.fragrance.entity.Fragrance; import server.acode.domain.fragrance.repository.FragranceRepository; -import server.acode.domain.user.dto.response.DisplayScrap; +import server.acode.domain.user.dto.response.ScrapDto; import server.acode.domain.user.entity.Scrap; import server.acode.domain.user.entity.User; @@ -19,10 +20,10 @@ import static org.assertj.core.api.Assertions.*; @SpringBootTest @Transactional +@ActiveProfiles("test") class ScrapRepositoryImplTest { @Autowired EntityManager em; - @Autowired ScrapRepository scrapRepository; @Autowired UserRepository userRepository; @Autowired FragranceRepository fragranceRepository; @@ -31,39 +32,19 @@ class ScrapRepositoryImplTest { @Test public void getScrap() { //given - User user = User.builder().nickname("testnickname").authKey("testauthkey").build(); - em.persist(user); - - Brand brand = Brand.builder().korName("testBrand").build(); - em.persist(brand); - - Fragrance testFragrance1 = Fragrance.builder() - .name("testFragrance1") - .thumbnail("testthumbnail1") - .concentration(Concentration.EDC) - .brand(brand) - .build(); + User user = userRepository.findById(1L).get(); + Fragrance fragrance = fragranceRepository.findById(1L).get(); - Fragrance testFragrance2 = Fragrance.builder() - .name("testFragrance2") - .thumbnail("testthumbnail2") - .concentration(Concentration.EDC) - .brand(brand) - .build(); - em.persist(testFragrance1); - em.persist(testFragrance2); - - Scrap scrap = new Scrap(user, testFragrance2); + Scrap scrap = new Scrap(user, fragrance); em.persist(scrap); // when PageRequest pageRequest = PageRequest.of(0, 10); - Page results = scrapRepository.getScrap(user.getId(), pageRequest); + Page results = scrapRepository.getScrap(1L, pageRequest); //then assertThat(results.getTotalElements()).isEqualTo(1); assertThat(results.getTotalPages()).isEqualTo(1); - assertThat(results.getContent()).extracting("fragranceId").containsExactly(testFragrance2.getId()); - + assertThat(results.getContent()).extracting("fragranceId").containsExactly(1L); } } \ No newline at end of file diff --git a/src/test/java/server/acode/domain/user/service/UserServiceTest.java b/src/test/java/server/acode/domain/user/service/UserServiceTest.java index fd6fab5..ad5a78d 100644 --- a/src/test/java/server/acode/domain/user/service/UserServiceTest.java +++ b/src/test/java/server/acode/domain/user/service/UserServiceTest.java @@ -3,22 +3,21 @@ import jakarta.persistence.EntityManager; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import org.mockito.Mockito; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; import org.springframework.transaction.annotation.Transactional; -import org.springframework.transaction.support.TransactionTemplate; import server.acode.domain.user.entity.User; import server.acode.domain.user.repository.UserRepository; -import server.acode.global.common.ErrorCode; import server.acode.global.exception.CustomException; -import java.util.Optional; +import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static org.assertj.core.api.Assertions.*; +@ActiveProfiles("test") @SpringBootTest @Transactional class UserServiceTest { @@ -27,6 +26,18 @@ class UserServiceTest { @Autowired UserService userService; @Autowired UserRepository userRepository; + @DisplayName("테스트 DB 분리") + @Test + void findAll(){ + List findUsers = userRepository.findAll(); + + + assertThat(findUsers.size()).isEqualTo(2); + assertThat(findUsers).extracting(User::getAuthKey) + .contains("testauthkey1", "testauthkey2"); + + } + @DisplayName("동시에 같은 닉네임으로 변경 요청") @Test void updateNickname() throws InterruptedException { @@ -39,8 +50,13 @@ void updateNickname() throws InterruptedException { //when service.execute(() -> { - userService.synchronizedUpdateNickname("same", 1L); - latch.countDown(); + try { + userService.synchronizedUpdateNickname("same", 1L); + } catch (CustomException e) { + + } finally { + latch.countDown(); + } }); service.execute(() -> { try { @@ -58,9 +74,7 @@ void updateNickname() throws InterruptedException { //then User user1 = userRepository.findById(1L).get(); User user2 = userRepository.findById(2L).get(); - System.out.println("user1 = " + user1.getNickname()); - System.out.println("user2 = " + user2.getNickname()); - assertThat(user1.getNickname()).isNotEqualTo(user2.getNickname()); + assertThat(user1.getNickname()).isNotEqualTo(user2.getNickname()); } } \ No newline at end of file diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml new file mode 100644 index 0000000..9077399 --- /dev/null +++ b/src/test/resources/application-test.yml @@ -0,0 +1,26 @@ +spring: + config: + activate: + on-profile: test + h2: + console: + enabled: true + jpa: + database: h2 + generate-ddl: off + database-platform: org.hibernate.dialect.MySQL5InnoDBDialect + hibernate: + ddl-auto: none + properties: + hibernate: + globally_quoted_identifiers: true + show_sql: true + format_sql: true + datasource: + driver-class-name: org.h2.Driver + url: jdbc:h2:mem:test;MODE=MySQL; + username: SA + password: + initialization-mode: always + schema: classpath:schema.sql + data: classpath:data.sql diff --git a/src/test/resources/data.sql b/src/test/resources/data.sql new file mode 100644 index 0000000..0b50003 --- /dev/null +++ b/src/test/resources/data.sql @@ -0,0 +1,14 @@ +insert into `user` (user_id, role, auth_key, nickname, review_cnt, is_del) values (1L, 'ROLE_USER', 'testauthkey1', 'nickname1', 0, 0); +insert into `user` (user_id, role, auth_key, nickname, review_cnt, is_del) values (2L, 'ROLE_USER', 'testauthkey2', 'nickname2', 0, 0); + +insert into brand(brand_id, kor_name) values (1L, '톰포드'); +insert into fragrance(fragrance_id, name, brand_id, concentration, rate_sum, review_cnt) values (1L, '톰포드향수', 1L, 'EDC', 8, 2); + +insert into review(rate, comment, season, longevity, intensity, style, user_id, fragrance_id) +values (5, '아주 좋아요', 'SPRING', 'ONEHOUR', 'WEAK', 'CHIC', 1L, 1L), + (3, '그냥 좋아요', 'SPRING', 'ONEHOUR', 'WEAK', 'CHIC', 2L, 1L); + +insert into review_intensity(review_intensity_id, fragrance_id, weak) values (1L, 1L, 2); +insert into review_longevity(review_longevity_id, fragrance_id, onehour) values (1L, 1L, 2); +insert into review_season(review_season_id, fragrance_id, spring) values (1L, 1L, 2); +insert into review_style(review_style_id, fragrance_id, chic) values (1L, 1L, 2); \ No newline at end of file diff --git a/src/test/resources/schema.sql b/src/test/resources/schema.sql new file mode 100644 index 0000000..a2d28ae --- /dev/null +++ b/src/test/resources/schema.sql @@ -0,0 +1,151 @@ +create table `user`( + user_id bigint primary key, + role varchar(50), + auth_key varchar(30), + nickname varchar(30), + review_cnt integer, + is_del tinyint default 0, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +CREATE TABLE brand ( + brand_id BIGINT PRIMARY KEY, + kor_name VARCHAR(50), + eng_name VARCHAR(50), + summary TEXT, + keyword VARCHAR(255), + background_img VARCHAR(255), + round_img VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); + +create table fragrance( + fragrance_id bigint primary key, + name varchar(50), + brand_id BIGINT, + rate_sum INT DEFAULT 0, + review_cnt INT DEFAULT 0, + concentration varchar(50), + is_single TINYINT DEFAULT 0, + view INT DEFAULT 0, + poster VARCHAR(255), + link1 VARCHAR(255), + link2 VARCHAR(255), + link3 VARCHAR(255), + thumbnail VARCHAR(255), + image1 VARCHAR(255), + image2 VARCHAR(255), + style VARCHAR(255), + season VARCHAR(255), + scent VARCHAR(255), + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + CONSTRAINT fk_brand FOREIGN KEY (brand_id) REFERENCES brand(brand_id) +); + +CREATE TABLE review ( + review_id BIGINT AUTO_INCREMENT PRIMARY KEY, + rate INT NOT NULL, + comment VARCHAR(100) NOT NULL, + season varchar(50), + longevity varchar(50), + intensity varchar(50), + style VARCHAR(20), + text_review VARCHAR(4000), + thumbnail VARCHAR(255), + image1 VARCHAR(255), + image2 VARCHAR(255), + is_del TINYINT DEFAULT 0, + user_id BIGINT, + fragrance_id BIGINT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES `user`(user_id), + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); + +CREATE TABLE review_intensity ( + review_intensity_id BIGINT PRIMARY KEY, + weak INT DEFAULT 0, + medium INT DEFAULT 0, + strong INT DEFAULT 0, + intense INT DEFAULT 0, + fragrance_id BIGINT, + version BIGINT DEFAULT 0L, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); + +CREATE TABLE review_longevity ( + review_longevity_id BIGINT PRIMARY KEY, + onehour INT DEFAULT 0, + fourhours INT DEFAULT 0, + halfday INT DEFAULT 0, + fullday INT DEFAULT 0, + fragrance_id BIGINT, + version BIGINT DEFAULT 0L, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); + + +CREATE TABLE review_season ( + review_season_id BIGINT PRIMARY KEY, + spring INT DEFAULT 0, + summer INT DEFAULT 0, + autumn INT DEFAULT 0, + winter INT DEFAULT 0, + all_seasons INT DEFAULT 0, + fragrance_id BIGINT, + version BIGINT DEFAULT 0L, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); + + +CREATE TABLE review_style ( + review_style_id BIGINT PRIMARY KEY, + chic INT DEFAULT 0, + mature INT DEFAULT 0, + luxurious INT DEFAULT 0, + elegant INT DEFAULT 0, + masculine INT DEFAULT 0, + comfortable INT DEFAULT 0, + serene INT DEFAULT 0, + light INT DEFAULT 0, + neutral INT DEFAULT 0, + friendly INT DEFAULT 0, + clean INT DEFAULT 0, + sensual INT DEFAULT 0, + delicate INT DEFAULT 0, + lively INT DEFAULT 0, + lovely INT DEFAULT 0, + bright INT DEFAULT 0, + radiant INT DEFAULT 0, + feminine INT DEFAULT 0, + innocent INT DEFAULT 0, + weighty INT DEFAULT 0, + soft INT DEFAULT 0, + cozy INT DEFAULT 0, + fragrance_id BIGINT, + version BIGINT DEFAULT 0L, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); + +CREATE TABLE scrap ( + scrap_id BIGINT AUTO_INCREMENT PRIMARY KEY, + user_id BIGINT, + fragrance_id BIGINT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + modified_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES `user`(user_id), + FOREIGN KEY (fragrance_id) REFERENCES fragrance(fragrance_id) +); +