diff --git a/.github/workflows/cicd-dev.yml b/.github/workflows/cicd-dev.yml index f5333206..bec9e75b 100644 --- a/.github/workflows/cicd-dev.yml +++ b/.github/workflows/cicd-dev.yml @@ -122,9 +122,11 @@ jobs: echo "${{ secrets.DB_INIT_SQL }}" > init.sql echo "${{ secrets.DEV_REDIS_CONF }}" > ./redis/redis.conf echo "${{ secrets.DOCKER_COMPOSE }}" > docker-compose.yml + + echo ${{ secrets.DOCKERHUB_PASSWORD }} | docker login --username ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin docker image prune -af docker-compose pull docker-compose down - docker-compose up -d \ No newline at end of file + docker-compose up -d diff --git a/module-admin/src/main/java/com/kernel360/brand/code/BrandBusinessCode.java b/module-admin/src/main/java/com/kernel360/brand/code/BrandBusinessCode.java new file mode 100644 index 00000000..a9573bf7 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/code/BrandBusinessCode.java @@ -0,0 +1,39 @@ +package com.kernel360.brand.code; + +import com.kernel360.code.BusinessCode; +import org.springframework.http.HttpStatus; + +public enum BrandBusinessCode implements BusinessCode { + + SUCCESS_FOUND_BRAND_LIST(HttpStatus.OK.value(),"BMB001","브랜드 목록 조회 성공"), + SUCCESS_FOUND_EXACT_BRAND(HttpStatus.OK.value(),"BMB002","브랜드 상세 조회 성공"), + SUCCESS_CREATED_BRAND(HttpStatus.CREATED.value(), "BMB003","브랜드 추가 성공" ), + SUCCESS_DELETED_BRAND(HttpStatus.OK.value(), "BMB004","브랜드 삭제 성공"), + SUCCESS_UPDATED_BRAND(HttpStatus.OK.value(), "BMB005","브랜드 업데이트 성공" ); + + + private final int status; + private final String code; + private final String message; + + BrandBusinessCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-admin/src/main/java/com/kernel360/brand/code/BrandErrorCode.java b/module-admin/src/main/java/com/kernel360/brand/code/BrandErrorCode.java new file mode 100644 index 00000000..47742ed7 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/code/BrandErrorCode.java @@ -0,0 +1,34 @@ +package com.kernel360.brand.code; + +import com.kernel360.code.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum BrandErrorCode implements ErrorCode { + FAILED_CANNOT_FOUND_ANY_BRAND(HttpStatus.NOT_FOUND.value(), "EBC001", "브랜드 목록이 비어있음"), + FAILED_ALREADY_EXISTS_BRAND(HttpStatus.BAD_REQUEST.value(), "EBC002", "생성하려는 브랜드가 이미 존재함"), + FAILED_CANNOT_FOUND_EXACT_BRAND(HttpStatus.NOT_FOUND.value(), "EBC003", "변경하려는 브랜드가 존재하지 않음"); + private final int status; + private final String code; + private final String message; + + BrandErrorCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-admin/src/main/java/com/kernel360/brand/controller/BrandController.java b/module-admin/src/main/java/com/kernel360/brand/controller/BrandController.java new file mode 100644 index 00000000..49846fbd --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/controller/BrandController.java @@ -0,0 +1,97 @@ +package com.kernel360.brand.controller; + +import com.kernel360.brand.code.BrandBusinessCode; +import com.kernel360.brand.dto.BrandDto; +import com.kernel360.brand.service.BrandServiceImpl; +import com.kernel360.response.ApiResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@Slf4j +@RestController +@RequestMapping("/admin/brands") +@RequiredArgsConstructor +public class BrandController { + + private final BrandServiceImpl brandService; + + @GetMapping + public ResponseEntity> getBrandList() { + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_FOUND_BRAND_LIST, brandService.findAllBrand()); + } + + @GetMapping("/brand") + public ResponseEntity> findBrandById(@RequestBody BrandDto brandDto) { + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_FOUND_EXACT_BRAND, + brandService.findBrandByBrandId(brandDto.brandNo())); + } + + @GetMapping("/brandName") + public ResponseEntity> findBrandByBrandName(@RequestBody BrandDto brandDto) { + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_FOUND_EXACT_BRAND, + brandService.findBrandByBrandName(brandDto.brandName())); + } + + + @PostMapping("/brand") + public ResponseEntity> createBrand(@RequestBody BrandDto brandDto) { + brandService.createBrand(brandDto); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_CREATED_BRAND); + } + + @DeleteMapping("/brand") + public ResponseEntity> deleteBrand(@RequestBody BrandDto brandDto) { + brandService.deleteBrand(brandDto.brandNo()); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_DELETED_BRAND); + } + + @PutMapping("/brand") + public ResponseEntity> updateAll(@RequestBody BrandDto brandDto) { + brandService.updateBrand(brandDto); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_UPDATED_BRAND); + } + + @PatchMapping("/description") + public ResponseEntity> updateBrandDescription(@RequestBody BrandDto brandDto) { + brandService.updateBrandDescription(brandDto.brandNo(), brandDto.description()); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_UPDATED_BRAND); + } + + @PatchMapping("/brandName") + public ResponseEntity> updateBrandName(@RequestBody BrandDto brandDto) { + brandService.updateBrandName(brandDto.brandNo(), brandDto.brandName()); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_UPDATED_BRAND); + } + + @PatchMapping("/companyName") + public ResponseEntity> updateBrandCompanyName(@RequestBody BrandDto brandDto) { + brandService.updateBrandCompanyName(brandDto.brandNo(), brandDto.companyName()); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_UPDATED_BRAND); + } + + @PatchMapping("/nationName") + public ResponseEntity> updateBrandNationName(@RequestBody BrandDto brandDto) { + brandService.updateBrandNationName(brandDto.brandNo(), brandDto.nationName()); + + return ApiResponse.toResponseEntity(BrandBusinessCode.SUCCESS_UPDATED_BRAND); + } +} diff --git a/module-admin/src/main/java/com/kernel360/brand/dto/BrandDto.java b/module-admin/src/main/java/com/kernel360/brand/dto/BrandDto.java new file mode 100644 index 00000000..2f7a22a2 --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/dto/BrandDto.java @@ -0,0 +1,20 @@ +package com.kernel360.brand.dto; + +import com.kernel360.brand.entity.Brand; + +public record BrandDto(Long brandNo, String brandName, String companyName, String description, String nationName) { + + public static BrandDto of(Long brandNo, String brandName, String companyName, String description, String nationName) { + return new BrandDto(brandNo, brandName, companyName, description, nationName); + } + + public static BrandDto of(String brandName, String companyName, String description, String nationName) { + return new BrandDto(null, brandName, companyName, description, nationName); + } + + public static BrandDto fromEntity(Brand brand) { + return new BrandDto(brand.getBrandNo(), brand.getBrandName(), brand.getCompanyName(), brand.getDescription(), + brand.getNationName()); + } + +} diff --git a/module-admin/src/main/java/com/kernel360/brand/service/BrandService.java b/module-admin/src/main/java/com/kernel360/brand/service/BrandService.java new file mode 100644 index 00000000..26f8dfdd --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/service/BrandService.java @@ -0,0 +1,28 @@ +package com.kernel360.brand.service; + +import com.kernel360.brand.dto.BrandDto; +import java.util.List; +import org.springframework.data.crossstore.ChangeSetPersister.NotFoundException; + +public interface BrandService { + + List findAllBrand(); + + BrandDto findBrandByBrandId(Long brandNo); + + BrandDto findBrandByBrandName(String brandName); + + void createBrand(BrandDto brandDto); + + void deleteBrand(final Long brandNo); + + void updateBrand(BrandDto brandDto); + + void updateBrandDescription(final Long brandNo, final String description); + + void updateBrandName(final Long brandNo, final String brandName); + + void updateBrandCompanyName(final Long brandNo, final String companyName); + + void updateBrandNationName(final Long brandNo, final String nationName); +} diff --git a/module-admin/src/main/java/com/kernel360/brand/service/BrandServiceImpl.java b/module-admin/src/main/java/com/kernel360/brand/service/BrandServiceImpl.java new file mode 100644 index 00000000..289ee9bc --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/brand/service/BrandServiceImpl.java @@ -0,0 +1,133 @@ +package com.kernel360.brand.service; + +import com.kernel360.brand.code.BrandErrorCode; +import com.kernel360.brand.dto.BrandDto; +import com.kernel360.brand.entity.Brand; +import com.kernel360.brand.repository.BrandRepository; +import com.kernel360.exception.BusinessException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BrandServiceImpl implements BrandService { + + private final BrandRepository brandRepository; + + @Override + public List findAllBrand() { + List brandList = brandRepository.findAll(); + if (brandList.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_ANY_BRAND); + } + + return brandList.stream() + .map(BrandDto::fromEntity) + .collect(Collectors.toList()); + } + + @Override + public BrandDto findBrandByBrandId(Long brandNo) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + return BrandDto.fromEntity(brand.get()); + } + + @Override + public BrandDto findBrandByBrandName(String brandName) { + Brand brand = brandRepository.findBrandByBrandName(brandName) + .orElseThrow(() -> new BusinessException( + BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND)); + + return BrandDto.fromEntity(brand); + } + + @Override + @Transactional + public void createBrand(BrandDto brandDto) { + Optional brand = brandRepository.findBrandByBrandName(brandDto.brandName()); + if (brand.isPresent()) { + throw new BusinessException(BrandErrorCode.FAILED_ALREADY_EXISTS_BRAND); + } + + brandRepository.save(Brand.toEntity(brandDto.brandName(), brandDto.companyName(), brandDto.description(), brandDto.nationName())); + } + + @Override + @Transactional + public void deleteBrand(Long brandNo) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brandRepository.delete(brand.get()); + } + + @Override + @Transactional + public void updateBrand(BrandDto brandDto) { + Optional brand = brandRepository.findBrandByBrandNo(brandDto.brandNo()); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brand.get() + .updateAll(brandDto.brandName(), brandDto.companyName(), brandDto.description(), brandDto.nationName()); + } + + @Override + @Transactional + public void updateBrandDescription(Long brandNo,String description) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brand.get().updateDescription(description); + } + + @Override + @Transactional + public void updateBrandName(Long brandNo, String brandName) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brand.get().updateBrandName(brandName); + } + + @Override + @Transactional + public void updateBrandCompanyName(Long brandNo, String companyName) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brand.get().updateBrandCompanyName(companyName); + } + + @Override + @Transactional + public void updateBrandNationName(Long brandNo, String nationName) { + Optional brand = brandRepository.findBrandByBrandNo(brandNo); + if (brand.isEmpty()) { + throw new BusinessException(BrandErrorCode.FAILED_CANNOT_FOUND_EXACT_BRAND); + } + + brand.get().updateBrandNationName(nationName); + } + + +} diff --git a/module-admin/src/main/java/com/kernel360/config/AdminAuditConfig.java b/module-admin/src/main/java/com/kernel360/config/AdminAuditConfig.java new file mode 100644 index 00000000..d1cc5dad --- /dev/null +++ b/module-admin/src/main/java/com/kernel360/config/AdminAuditConfig.java @@ -0,0 +1,18 @@ +package com.kernel360.config; + +import jakarta.servlet.http.HttpServletRequest; +import java.util.Optional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.domain.AuditorAware; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +@Configuration +public class AdminAuditConfig implements AuditorAware { + @Override + public Optional getCurrentAuditor() { + HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest(); + String createId = Optional.ofNullable(request.getParameter("id")).orElse("module-admin"); + + return Optional.of(createId); + } +} diff --git a/module-api/build.gradle b/module-api/build.gradle index 52194452..c8ea3de5 100644 --- a/module-api/build.gradle +++ b/module-api/build.gradle @@ -78,6 +78,12 @@ dependencies { //mybatis implementation group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '3.0.3' + + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' } tasks.named('test') { diff --git a/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java b/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java index df2ede03..4ef9d5d9 100644 --- a/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java +++ b/module-api/src/main/java/com/kernel360/global/Interceptor/AcceptInterceptor.java @@ -6,10 +6,13 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpMethod; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import org.springframework.web.servlet.HandlerInterceptor; +import java.util.Objects; + @Component @RequiredArgsConstructor public class AcceptInterceptor implements HandlerInterceptor { @@ -18,6 +21,10 @@ public class AcceptInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { + if (validateTargetUri(request)) { + return true; + } + boolean result = true; String requestToken = request.getHeader("Authorization"); @@ -33,4 +40,22 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons return result; } + private boolean validateTargetUri(HttpServletRequest request) { + String requestURI = request.getRequestURI(); + String method = request.getMethod(); + + if (Objects.isNull(requestURI)) { + return false; + } + + if (!requestURI.startsWith("/reviews")) { + return false; + } + + if (HttpMethod.GET.matches(method)) { + return true; + } + + return false; + } } diff --git a/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java b/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java index 2ba476d2..85aa2a07 100644 --- a/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java +++ b/module-api/src/main/java/com/kernel360/global/Interceptor/InterceptorConfig.java @@ -13,8 +13,10 @@ public class InterceptorConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(acceptInterceptor) - .addPathPatterns("/auth/**"); //** 인증 JWT 토큰 관련 **// -// .addPathPatterns("/mypage/**"); + .addPathPatterns("/auth/**") //** 인증 JWT 토큰 관련 **// + .addPathPatterns("/reviews/**") + .addPathPatterns("/likes/**") + .addPathPatterns("/mypage/**"); //.excludePathPatterns("/public/**"); // 제외할 URL 패턴 } diff --git a/module-api/src/main/java/com/kernel360/global/config/WebConfig.java b/module-api/src/main/java/com/kernel360/global/config/WebConfig.java index ee5e13d9..14602d0c 100644 --- a/module-api/src/main/java/com/kernel360/global/config/WebConfig.java +++ b/module-api/src/main/java/com/kernel360/global/config/WebConfig.java @@ -26,7 +26,7 @@ public void addCorsMappings(CorsRegistry registry) { .allowedHeaders("*") .allowedMethods("*") .allowCredentials(true) - .allowedOrigins("https://www.washfit.site", "https://dev.washfit.site") + .allowedOrigins("https://www.washfit.site", "https://dev.washfit.site", "http://localhost:3000", "https://washfit.vercel.app", "https://devapi.washfit.site") .maxAge(3600); } } diff --git a/module-api/src/main/java/com/kernel360/likes/service/LikeService.java b/module-api/src/main/java/com/kernel360/likes/service/LikeService.java index 519c2fba..72ff4938 100644 --- a/module-api/src/main/java/com/kernel360/likes/service/LikeService.java +++ b/module-api/src/main/java/com/kernel360/likes/service/LikeService.java @@ -5,16 +5,17 @@ import com.kernel360.likes.entity.Like; import com.kernel360.likes.repository.LikeRepository; import com.kernel360.member.service.MemberService; -import com.kernel360.product.code.ProductsErrorCode; import com.kernel360.product.dto.ProductDto; -import com.kernel360.product.entity.Product; import com.kernel360.product.repository.ProductRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.List; +import java.util.Optional; @Service @@ -28,10 +29,8 @@ public class LikeService { @Transactional public void heartOn(Long productNo, String token) { Long memberNo = memberService.findMemberByToken(token).memberNo(); - Product product = productRepository.findById(productNo) - .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT)); - likeRepository.save(Like.of(memberNo, product.getProductNo())); + likeRepository.save(Like.of(memberNo, productNo)); } @Transactional @@ -46,11 +45,14 @@ public void heartOff(Long productNo, String token) { @Transactional(readOnly = true) public Page findAllLikes(String token, Pageable pageable) { Long memberNo = memberService.findMemberByToken(token).memberNo(); - - return likeRepository.findAllByMemberNo(memberNo, pageable) - .map(like -> productRepository.findById(like.getId()) - .orElseThrow(() -> new BusinessException(ProductsErrorCode.NOT_FOUND_PRODUCT))) - .map(ProductDto::from); - + Page likesPage = likeRepository.findAllByMemberNo(memberNo, pageable); + List productDtos = likesPage.getContent().stream() + .map(like -> productRepository.findById(like.getProductNo())) + .filter(Optional::isPresent) + .map(Optional::get) + .map(ProductDto::from) + .toList(); + + return new PageImpl<>(productDtos, pageable, productDtos.size()); } } diff --git a/module-api/src/main/java/com/kernel360/main/controller/MainController.java b/module-api/src/main/java/com/kernel360/main/controller/MainController.java index 8eb4da28..01bb3308 100644 --- a/module-api/src/main/java/com/kernel360/main/controller/MainController.java +++ b/module-api/src/main/java/com/kernel360/main/controller/MainController.java @@ -17,6 +17,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + @RestController @RequestMapping @@ -31,11 +33,18 @@ ResponseEntity> getBanner() { return ApiResponse.toResponseEntity(BannerBusinessCode.GET_BANNER_DATA_SUCCESS, mainService.getBanner()); } +// @GetMapping("/recommend-products") +// ResponseEntity>> getRecommendProducts(Pageable pageable) { +// Page recommendProducts = productService.getRecommendProducts(pageable); +// +// return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_RECOMMEND_PRODUCT_DATA_SUCCESS, recommendProducts); +// +// } + @GetMapping("/recommend-products") - ResponseEntity>> getRecommendProducts(Pageable pageable) { - Page recommendProducts = productService.getRecommendProducts(pageable); + ResponseEntity>> getRecommendProducts() { - return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_RECOMMEND_PRODUCT_DATA_SUCCESS, recommendProducts); + return ApiResponse.toResponseEntity(ProductsBusinessCode.GET_RECOMMEND_PRODUCT_DATA_SUCCESS, productService.getRecommendProductsWithRandom()); } diff --git a/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java b/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java index 90c9be21..b7247107 100644 --- a/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java +++ b/module-api/src/main/java/com/kernel360/main/dto/RecommendProductsDto.java @@ -4,22 +4,25 @@ import com.kernel360.product.entity.Product; public record RecommendProductsDto( - Long id, + Long productNo, String imageSource, String alt, - String productName + String productName, + String item ) { public static RecommendProductsDto of( - Long id, + Long productNo, String imageSource, String alt, - String productName + String productName, + String item ) { return new RecommendProductsDto( - id, + productNo, imageSource, alt, - productName + productName, + item ); } @@ -30,7 +33,9 @@ public static RecommendProductsDto from(Product entity) { "src/main/resources/static/suggestSample.png", // FixMe:: entity.getImage() 같은걸로 변경해야 함 "제품 이미지", - entity.getProductName()); + entity.getProductName(), + entity.getItem() + ); } // public static ProductDto from(RecommendProductsDto){ diff --git a/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java b/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java index 1fb674c7..0ef71241 100644 --- a/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java +++ b/module-api/src/main/java/com/kernel360/member/code/MemberBusinessCode.java @@ -19,8 +19,8 @@ public enum MemberBusinessCode implements BusinessCode { SUCCESS_REQUEST_UPDATE_CAR_INFO_MEMBER(HttpStatus.OK.value(), "BMC010", "CarInfo 정보가 변경 되었습니다."), SUCCESS_REQUEST_FIND_MEMBER_ID(HttpStatus.OK.value(), "BMC011","회원 아이디 찾기 메일이 발송되었습니다."), SUCCESS_REQUEST_SEND_RESET_PASSWORD_EMAIL(HttpStatus.OK.value(), "BMC012", "회원 비밀번호 초기화 메일이 발송되었습니다."), - SUCCESS_REQUEST_RESET_PASSWORD_PAGE(HttpStatus.FOUND.value(), "BMC013", "비밀번호 초기화 토큰이 유효하므로 비밀번호 초기화 페이지로 접근합니다."), - SUCCESS_REQUEST_RESET_PASSWORD(HttpStatus.OK.value(), "BMC014", "비밀번호가 초기화되었습니다."), + SUCCESS_REQUEST_RESET_PASSWORD_PAGE(HttpStatus.OK.value(), "BMC013", "액세스 토큰이 유효하므로 비밀번호 초기화 페이지로 접근에 성공합니다."), + SUCCESS_REQUEST_RESET_PASSWORD(HttpStatus.OK.value(), "BMC014", "비밀번호가 성공적으로 초기화되었습니다."), SUCCESS_REQUEST_LOGIN_MEMBER_KAKAO(HttpStatus.OK.value(), "BMC015", "로그인 성공"), SUCCESS_REQUEST_SIGN_OUT_MEMBER(HttpStatus.OK.value(), "BMC016", "회원탈퇴 성공"), SUCCESS_FIND_WASH_INFO_IN_MEMBER(HttpStatus.OK.value(), "BMC017","세차정보 조회 성공"); diff --git a/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java b/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java index facf582c..df300656 100644 --- a/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java +++ b/module-api/src/main/java/com/kernel360/member/code/MemberErrorCode.java @@ -4,8 +4,6 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.HttpStatus; -import java.util.EnumSet; - @RequiredArgsConstructor public enum MemberErrorCode implements ErrorCode { @@ -16,7 +14,7 @@ public enum MemberErrorCode implements ErrorCode { FAILED_GENERATE_LOGIN_REQUEST_INFO(HttpStatus.INTERNAL_SERVER_ERROR.value(), "EMC005", "정보 불일치로 인한 로그인 정보 생성 실패"), FAILED_REQUEST_LOGIN(HttpStatus.BAD_REQUEST.value(), "EMC006", "정보 불일치로 인한 로그인 실패"), FAILED_FIND_MEMBER_INFO(HttpStatus.BAD_REQUEST.value(), "EMC007", "요청 회원정보가 존재하지 않습니다."), - EXPIRED_PASSWORD_RESET_TOKEN(HttpStatus.NOT_FOUND.value(), "EMC008", "유효하지 않은 비밀번호 초기화 토큰입니다"), + EXPIRED_TOKEN(HttpStatus.NOT_FOUND.value(), "EMC008", "유효하지 않은 토큰입니다"), FAILED_REQUEST_LOGIN_FOR_KAKAO(HttpStatus.BAD_REQUEST.value(), "EMC009", "카카오 로그인 정보를 찾을 수 없습니다."), FAILED_FIND_MEMBER_CAR_INFO(HttpStatus.BAD_REQUEST.value(), "EMC010", "요청 회원의 차량정보가 존재하지 않습니다."), FAILED_FIND_MEMBER_WASH_INFO(HttpStatus.BAD_REQUEST.value(), "EMC011", "요청 회원의 세차정보가 존재하지 않습니다."), diff --git a/module-api/src/main/java/com/kernel360/member/controller/MemberController.java b/module-api/src/main/java/com/kernel360/member/controller/MemberController.java index 18f7c26f..f85a50d2 100644 --- a/module-api/src/main/java/com/kernel360/member/controller/MemberController.java +++ b/module-api/src/main/java/com/kernel360/member/controller/MemberController.java @@ -1,7 +1,9 @@ package com.kernel360.member.controller; -import com.fasterxml.jackson.core.JsonProcessingException; +import static com.kernel360.member.code.MemberBusinessCode.SUCCESS_REQUEST_JOIN_MEMBER_CREATED; +import static com.kernel360.member.code.MemberBusinessCode.SUCCESS_REQUEST_LOGIN_MEMBER; + import com.kernel360.carinfo.entity.CarInfo; import com.kernel360.member.code.MemberBusinessCode; import com.kernel360.member.dto.CarInfoDto; @@ -15,10 +17,17 @@ import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import static com.kernel360.member.code.MemberBusinessCode.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; @Slf4j @RestController @@ -49,26 +58,26 @@ public ResponseEntity> login(@RequestBody MemberDto login } @GetMapping("/duplicatedCheckId/{id}") - public boolean duplicatedCheckId(@PathVariable String id) { + public boolean duplicatedCheckId(@PathVariable("id") String id) { return memberService.idDuplicationCheck(id); } @GetMapping("/duplicatedCheckEmail/{email}") - public boolean duplicatedCheckEmail(@PathVariable String email) { + public boolean duplicatedCheckEmail(@PathVariable("email") String email) { return memberService.emailDuplicationCheck(email); } @PostMapping("/wash") - public ResponseEntity> saveWashInfo(@RequestBody WashInfoDto washInfo, @RequestHeader("Authorization") String authToken){ + public ResponseEntity> saveWashInfo(@RequestBody WashInfoDto washInfo, @RequestHeader("Authorization") String authToken) { memberService.saveWashInfo(washInfo, authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_UPDATE_WASH_INFO_MEMBER); } @PostMapping("/car") - public ResponseEntity> saveCarInfo(@RequestBody CarInfoDto carInfo, @RequestHeader("Authorization") String authToken){ + public ResponseEntity> saveCarInfo(@RequestBody CarInfoDto carInfo, @RequestHeader("Authorization") String authToken) { memberService.saveCarInfo(carInfo, authToken); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_UPDATE_CAR_INFO_MEMBER); @@ -88,41 +97,50 @@ public ResponseEntity> sendPasswordResetUriByEmail(@RequestB //--입력받은 아이디를 데이터베이스에 조회, 없으면 예외 발생--/ MemberDto memberDto = memberService.findByMemberId(dto.memberId()); //--유효성이 검증된 아이디에 대해서 만료시간이 있는 비밀번호 초기화 (호스트 + UUID) 링크 생성 --// - String resetUri = findCredentialService.generatePasswordResetUri( memberDto); + String resetUri = findCredentialService.generatePasswordResetPageUri(memberDto); //-- 가입시 입력한 이메일로 비밀번호 초기화 이메일 발송 --// findCredentialService.sendPasswordResetUri(resetUri, memberDto); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_SEND_RESET_PASSWORD_EMAIL); } + /** + * @param accessToken 재설정 페이지로의 액세스 토큰 + * @return 비밀번호 재설정 페이지로 재설정 토큰을 URL 쿼리에 담아 리다이렉트 + */ + @GetMapping("/find-password") + public ResponseEntity> redirectToPasswordResetPage(@RequestParam("token") String accessToken) { + findCredentialService.getData(accessToken); + HttpHeaders headers = findCredentialService.setRedirectLocation(accessToken); + + return new ResponseEntity<>(headers, HttpStatus.FOUND); + } + + /** + * @param resetToken 비밀번호 재설정 토큰 + * @return 성공시 200 + */ @GetMapping("/reset-password") - public ResponseEntity> getPasswordResetPage(@RequestParam String token) { - findCredentialService.getData(token); + public ResponseEntity> getPasswordResetPage(@RequestParam("token") String resetToken) { + findCredentialService.getData(resetToken); - return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_RESET_PASSWORD_PAGE, token); + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_RESET_PASSWORD_PAGE); } @PostMapping("/reset-password") public ResponseEntity> resetPassword(@RequestBody MemberCredentialDto credentialDto) { - String authKey = findCredentialService.resetPassword(credentialDto); - findCredentialService.getAndExpireData(authKey); + String token = findCredentialService.resetPassword(credentialDto); + findCredentialService.getAndExpireData(token); return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_RESET_PASSWORD); } @GetMapping("/login/forKakao") - public ResponseEntity> loginForKakao(@RequestHeader("Authorization") String accessToken,HttpServletRequest request) { + public ResponseEntity> loginForKakao(@RequestHeader("Authorization") String accessToken, HttpServletRequest request) { - MemberDto member = memberService.loginForKakao(accessToken,request); + MemberDto member = memberService.loginForKakao(accessToken, request); return ApiResponse.toResponseEntity(SUCCESS_REQUEST_LOGIN_MEMBER, member); } - @GetMapping("/signout") - public ResponseEntity> signOut(@RequestHeader("Authorization") String accessToken) { - - memberService.signOut(accessToken); - - return ApiResponse.toResponseEntity(SUCCESS_REQUEST_SIGN_OUT_MEMBER); - } } diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java b/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java index 4601f95d..55da61cf 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberCredentialDto.java @@ -2,9 +2,9 @@ import com.fasterxml.jackson.annotation.JsonProperty; -public record MemberCredentialDto(@JsonProperty("authToken") String authToken, +public record MemberCredentialDto(@JsonProperty("token") String authToken, @JsonProperty("email") String email, - @JsonProperty("memberId") String memberId, + @JsonProperty("id") String memberId, @JsonProperty("password") String password) { public static MemberCredentialDto of(String authToken, String email, String memberId, String password) { diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java index 7f7bee9e..6c5b9af9 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberDto.java @@ -89,7 +89,8 @@ public Member toEntity() { this.email(), this.password(), Gender.valueOf(this.gender()).ordinal(), - Age.valueOf(this.age()).ordinal() + Age.valueOf(this.age()).ordinal(), + null ); } diff --git a/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java b/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java index 8170602f..c82bc74e 100644 --- a/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java +++ b/module-api/src/main/java/com/kernel360/member/dto/MemberInfo.java @@ -1,10 +1,8 @@ package com.kernel360.member.dto; - - public record MemberInfo( - int gender, - int age + int gender, + int age ) { } \ No newline at end of file diff --git a/module-api/src/main/java/com/kernel360/member/enumset/AccountType.java b/module-api/src/main/java/com/kernel360/member/enumset/AccountType.java new file mode 100644 index 00000000..3ceb842c --- /dev/null +++ b/module-api/src/main/java/com/kernel360/member/enumset/AccountType.java @@ -0,0 +1,5 @@ +package com.kernel360.member.enumset; + +public enum AccountType { + PLATFORM, KAKAO, NAVER, GOOGLE +} diff --git a/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java b/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java index fc3e4f1b..a423d6b5 100644 --- a/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java +++ b/module-api/src/main/java/com/kernel360/member/service/FindCredentialService.java @@ -4,6 +4,7 @@ import com.kernel360.member.code.MemberErrorCode; import com.kernel360.member.dto.MemberCredentialDto; import com.kernel360.member.dto.MemberDto; +import java.net.URI; import java.time.Duration; import java.util.Objects; import java.util.UUID; @@ -11,6 +12,7 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.ValueOperations; +import org.springframework.http.HttpHeaders; import org.springframework.stereotype.Service; import org.springframework.web.util.UriComponentsBuilder; @@ -82,7 +84,7 @@ public void sendPasswordResetUri(String resetUri, MemberDto dto) { + "
\n" + "

고객센터 운영시간

\n" + "

평일 10:00~19:00 (주말 및 공휴일 제외/ 점심시간 13:00~14:00)

\n" - + " " + "고객센터 문의하기\n" + "
\n" @@ -98,28 +100,28 @@ public void sendPasswordResetUri(String resetUri, MemberDto dto) { emailService.sendMail(dto.email(), "[No-Reply] Wash-Fit 아이디/비밀번호 찾기", htmlContent); } - public String generatePasswordResetUri(MemberDto memberDto) { - String resetToken = generateUUID(); + public String generatePasswordResetPageUri(MemberDto memberDto) { + String accessToken = generateUUID(); String uriString = UriComponentsBuilder.fromHttpUrl(HOST_HTTP_URL) - .path("/member/reset-password") - .queryParam("token", resetToken) + .path("/member/find-password") + .queryParam("token", accessToken) .build() .toUriString(); - setExpiringData(resetToken, memberDto.id(), TOKEN_DURATION); // duration 값 상수로 변경관리 필요 + setExpiringData(accessToken, memberDto.id(), TOKEN_DURATION); // duration 값 상수로 변경관리 필요 return uriString; } public String resetPassword(MemberCredentialDto credentialDto) { ValueOperations valueOperations = redisTemplate.opsForValue(); - String value = valueOperations.get(credentialDto.authToken()); + String memberId = valueOperations.get(credentialDto.authToken()); - if (Objects.isNull(value)) { - throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + if (Objects.isNull(memberId)) { + throw new BusinessException(MemberErrorCode.EXPIRED_TOKEN); } - memberService.resetPasswordByMemberId(value, credentialDto.password()); + memberService.resetPasswordByMemberId(memberId, credentialDto.password()); return credentialDto.authToken(); } @@ -133,7 +135,7 @@ private String generateUUID() { public String getData(String key) { ValueOperations valueOperations = redisTemplate.opsForValue(); if (Objects.isNull(valueOperations.get(key))) { - throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + throw new BusinessException(MemberErrorCode.EXPIRED_TOKEN); } return valueOperations.get(key); @@ -156,9 +158,31 @@ public void setExpiringData(String key, String value, int duration) { public void getAndExpireData(String key) { ValueOperations valueOperations = redisTemplate.opsForValue(); if (Objects.isNull(valueOperations.get(key))) { - throw new BusinessException(MemberErrorCode.EXPIRED_PASSWORD_RESET_TOKEN); + throw new BusinessException(MemberErrorCode.EXPIRED_TOKEN); } valueOperations.getAndDelete(key); } + + public HttpHeaders setRedirectLocation(String accessToken) { + //** 액세스 토큰에서 아이디를 추출하고 + String memberId = getData(accessToken); + //** 액세스 토큰 만료처리 -> 할지 말지 고민해봐야 + getAndExpireData(accessToken); + //** 비밀번호 재설정용 토큰을 발급 + String resetToken = generateUUID(); + //** 재설정 토큰 만료기간 설정 + setExpiringData(resetToken, memberId, TOKEN_DURATION); + + + String uriString = UriComponentsBuilder.fromHttpUrl(HOST_HTTP_URL) + .path("/member/reset-password") + .queryParam("token", resetToken) + .build() + .toUriString(); + HttpHeaders headers = new HttpHeaders(); + headers.setLocation(URI.create(uriString)); + + return headers; + } } diff --git a/module-api/src/main/java/com/kernel360/member/service/MemberService.java b/module-api/src/main/java/com/kernel360/member/service/MemberService.java index c7b7e7b3..2b97889e 100644 --- a/module-api/src/main/java/com/kernel360/member/service/MemberService.java +++ b/module-api/src/main/java/com/kernel360/member/service/MemberService.java @@ -9,6 +9,7 @@ import com.kernel360.member.dto.*; import com.kernel360.member.entity.Member; import com.kernel360.member.entity.WithdrawMember; +import com.kernel360.member.enumset.AccountType; import com.kernel360.member.enumset.Age; import com.kernel360.member.enumset.Gender; import com.kernel360.member.repository.MemberRepository; @@ -20,7 +21,6 @@ import jakarta.servlet.http.HttpServletRequest; import java.util.Map; import java.util.Objects; -import java.util.Optional; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -63,7 +63,7 @@ protected Member getNewJoinMemberEntity(MemberDto requestDto) { throw new BusinessException(MemberErrorCode.FAILED_NOT_MAPPING_ENUM_VALUE_OF); } - return Member.createJoinMember(requestDto.id(), requestDto.email(), encodePassword, genderOrdinal, ageOrdinal); + return Member.createJoinMember(requestDto.id(), requestDto.email(), encodePassword, genderOrdinal, ageOrdinal, AccountType.PLATFORM.name()); } @Transactional @@ -78,6 +78,7 @@ public MemberDto login(MemberDto loginDto, HttpServletRequest request) { String loginToken = jwt.generateToken(memberEntity.getId()); + //TODO REFACTOR AUTH 정보를 RDB -> 래디스로 변경 authService.saveAuthByMember(memberEntity.getMemberNo(), ConvertSHA256.convertToSHA256(loginToken), request); return MemberDto.login(memberEntity, loginToken); @@ -91,14 +92,14 @@ private Member newRequestLoginEntity(MemberDto loginDto) { @Transactional(readOnly = true) public boolean idDuplicationCheck(String id) { - Member member = memberRepository.findOneById(id); + Member member = memberRepository.findOneByIdForAccountTypeByPlatform(id); return member != null; } @Transactional(readOnly = true) public boolean emailDuplicationCheck(String email) { - Member member = memberRepository.findOneByEmail(email); + Member member = memberRepository.findOneByEmailForAccountTypeByPlatform(email); return member != null; } @@ -123,18 +124,17 @@ public void deleteMember(String id) { } @Transactional - public void deleteMemberByToken(String token) { - final String id = JWT.ownerId(token); - Member member = memberRepository.findOneById(id); - //Fixme :: 멤버 탈퇴시, Deleted Table을 만들고, 데이터를 백업한후, 삭제하는 방식이나, MemberTable에 삭제여부를 표시하는 방식으로 리팩토링 필요 + public void deleteMemberByToken(String accessToken) { + Member member = memberRepository.findOneById(JWT.ownerId(accessToken)); + withdrawMemberRepository.save(WithdrawMember.of(member)); //of 받는식을 변경했습니다. 이 방식으로 리팩터를 하면 코드가 깔끔하네요. memberRepository.delete(member); - log.info("{} 회원 탈퇴 처리 완료", id); + log.info("{} 회원 탈퇴 처리 완료", accessToken); } @Transactional public void changePassword(String password, String token) { String id = JWT.ownerId(token); - Member member = memberRepository.findOneById(id); + Member member = memberRepository.findOneByIdForAccountTypeByPlatform(id); if (!member.getPassword().equals(ConvertSHA256.convertToSHA256(password))) { throw new BusinessException(MemberErrorCode.WRONG_PASSWORD_REQUEST); @@ -173,14 +173,24 @@ public Map getCarInfo(String token) { } @Transactional(readOnly = true) - public Optional getWashInfo(String token) { + public Map getWashInfo(String token) { String id = JWT.ownerId(token); Member member = memberRepository.findOneById(id); if (member == null) { throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); } + WashInfo washInfo = member.getWashInfo(); + if(washInfo == null){ + throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_WASH_INFO); + } - return Optional.of(WashInfoDto.from(member.getWashInfo())); + WashInfoDto washInfoDto = WashInfoDto.from(washInfo); + return Map.of( + "wash_info", washInfoDto, + "frequency_options", commonCodeService.getCodes("frequency"), + "cost_options", commonCodeService.getCodes("cost"), + "interest_options", commonCodeService.getCodes("interest") + ); } @Transactional @@ -229,7 +239,7 @@ public MemberDto findByEmail(String email) { @Transactional(readOnly = true) public MemberDto findByMemberId(String memberId) { - Member member = memberRepository.findOneById(memberId); + Member member = memberRepository.findOneByIdForAccountTypeByPlatform(memberId); if (member == null) { throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); } @@ -239,7 +249,7 @@ public MemberDto findByMemberId(String memberId) { @Transactional public void resetPasswordByMemberId(String memberId, String newPassword) { - Member member = memberRepository.findOneById(memberId); + Member member = memberRepository.findOneByIdForAccountTypeByPlatform(memberId); if (member == null) { throw new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_INFO); } @@ -253,28 +263,20 @@ public MemberDto loginForKakao(String accessToken, HttpServletRequest request) { KakaoUserDto kakaoUser = kakaoRequest.getKakaoUserByToken(accessToken); if (Objects.isNull(memberRepository.findOneById(kakaoUser.id()))) { memberRepository.save( - Member.createForKakao(kakaoUser.id(), kakaoUser.email(), "kakao", Gender.OTHERS.ordinal(), - Age.AGE_99.ordinal())); + Member.createJoinMember(kakaoUser.id(), kakaoUser.email(), "kakao", Gender.OTHERS.ordinal(), + Age.AGE_99.ordinal(), AccountType.KAKAO.name())); } MemberDto memberDto = MemberDto.from(memberRepository.findOneById(kakaoUser.id())); String loginToken = jwt.generateToken(memberDto.id()); + //TODO REFACTOR AUTH 정보를 RDB -> 래디스로 변경 authService.saveAuthByMember(memberDto.memberNo(), ConvertSHA256.convertToSHA256(loginToken), request); return MemberDto.fromKakao(memberDto, loginToken); } - @Transactional - public void signOut(String accessToken) { - Member member = memberRepository.findOneById(JWT.ownerId(accessToken)); - - withdrawMemberRepository.save(WithdrawMember.of(member.getMemberNo(),member.getId(), member.getEmail(), null)); - - memberRepository.delete(member); - } - @Transactional(readOnly = true) public boolean validatePassword(String password, String token) { String id = JWT.ownerId(token); diff --git a/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java b/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java index ec35ab86..4adccab2 100644 --- a/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java +++ b/module-api/src/main/java/com/kernel360/mypage/controller/MyPageController.java @@ -1,12 +1,9 @@ package com.kernel360.mypage.controller; -import com.kernel360.exception.BusinessException; import com.kernel360.member.code.MemberBusinessCode; -import com.kernel360.member.code.MemberErrorCode; import com.kernel360.member.dto.MemberDto; import com.kernel360.member.dto.MemberInfo; import com.kernel360.member.dto.PasswordDto; -import com.kernel360.member.dto.WashInfoDto; import com.kernel360.member.service.MemberService; import com.kernel360.product.service.ProductService; import com.kernel360.response.ApiResponse; @@ -40,14 +37,12 @@ ResponseEntity>> myCar(@RequestHeader("Authoriza } @GetMapping("/wash") - ResponseEntity> myWash(@RequestHeader("Authorization") String authToken) { - WashInfoDto washInfoDto = memberService.getWashInfo(authToken) - .orElseThrow(() -> new BusinessException(MemberErrorCode.FAILED_FIND_MEMBER_WASH_INFO)); + ResponseEntity>> myWash(@RequestHeader("Authorization") String authToken) { + Map washInfo = memberService.getWashInfo(authToken); - return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_FIND_WASH_INFO_IN_MEMBER, washInfoDto); + return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_FIND_WASH_INFO_IN_MEMBER, washInfo); } - @DeleteMapping("/member") ResponseEntity> memberDelete(@RequestHeader("Authorization") String authToken) { memberService.deleteMemberByToken(authToken); @@ -55,7 +50,6 @@ ResponseEntity> memberDelete(@RequestHeader("Authorization") S return ApiResponse.toResponseEntity(MemberBusinessCode.SUCCESS_REQUEST_DELETE_MEMBER); } - @PostMapping("/member") ResponseEntity> changePassword(@RequestBody String password, @RequestHeader("Authorization") String authToken) { memberService.changePassword(password, authToken); @@ -69,7 +63,6 @@ boolean validatePassword(@RequestBody PasswordDto password, @RequestHeader("Auth return memberService.validatePassword(password.password(), authToken); } - @PatchMapping("/member") ResponseEntity> updateMember(@RequestBody MemberInfo memberInfo, @RequestHeader("Authorization") String authToken) { diff --git a/module-api/src/main/java/com/kernel360/product/service/ProductService.java b/module-api/src/main/java/com/kernel360/product/service/ProductService.java index 7a97f03d..18cf4448 100644 --- a/module-api/src/main/java/com/kernel360/product/service/ProductService.java +++ b/module-api/src/main/java/com/kernel360/product/service/ProductService.java @@ -134,4 +134,8 @@ public Page getProductWithKeywordAndRecentOrder(String keyword, Page return productRepository.getProductWithKeywordAndRecentOrder(keyword, pageable) .map(ProductDto::from); } + + public List getRecommendProductsWithRandom() { + return productRepository.getRecommendProductsWithRandom().stream().map(RecommendProductsDto::from).toList(); + } } diff --git a/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java index 6ea8b524..7ff710ec 100644 --- a/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java +++ b/module-api/src/main/java/com/kernel360/review/code/ReviewErrorCode.java @@ -6,7 +6,8 @@ @RequiredArgsConstructor public enum ReviewErrorCode implements ErrorCode { - INVALID_STAR_RATING_VALUE(HttpStatus.BAD_REQUEST.value(), "ERV001", "유효하지 않은 별점입니다."); + INVALID_STAR_RATING_VALUE(HttpStatus.BAD_REQUEST.value(), "ERV001", "유효하지 않은 별점입니다."), + INVALID_REVIEW_WRITE_REQUEST(HttpStatus.BAD_REQUEST.value(), "ERV002", "리뷰가 중복되거나 유효하지 않습니다."); private final int status; private final String code; diff --git a/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java index 6ec02f5f..0eaddc7c 100644 --- a/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java +++ b/module-api/src/main/java/com/kernel360/review/controller/ReviewController.java @@ -5,11 +5,11 @@ import com.kernel360.review.dto.ReviewDto; import com.kernel360.review.service.ReviewService; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; -import java.util.List; - @RestController @RequiredArgsConstructor @RequestMapping("/reviews") @@ -17,18 +17,19 @@ public class ReviewController { private final ReviewService reviewService; - @GetMapping("/product/{productNo}") - public ResponseEntity>> getReviewsByProduct(@PathVariable Long productNo) { - List reviews = reviewService.getReviewsByProduct(productNo); + @GetMapping("") + public ResponseEntity>> getReviewsByProduct( + @RequestParam(name = "productNo") Long productNo, + @RequestParam(name = "sortBy", defaultValue = "reviewNo", required = false) String sortBy, + Pageable pageable) { - return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEWS, reviews); + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEWS, reviewService.getReviewsByProduct(productNo, sortBy, pageable)); } @GetMapping("/{reviewNo}") public ResponseEntity> getReview(@PathVariable Long reviewNo) { - ReviewDto review = reviewService.getReview(reviewNo); - return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEW, review); + return ApiResponse.toResponseEntity(ReviewBusinessCode.SUCCESS_GET_REVIEW, reviewService.getReview(reviewNo)); } @PostMapping("") diff --git a/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java new file mode 100644 index 00000000..4b7b52cc --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/dto/ReviewSearchDto.java @@ -0,0 +1,20 @@ +package com.kernel360.review.dto; + +public record ReviewSearchDto( + Long productNo, + Long memberNo, + String sortBy +) { + public static ReviewSearchDto of(Long productNo, Long memberNo, String sortBy) { + return new ReviewSearchDto(productNo, memberNo, sortBy); + } + + public static ReviewSearchDto byProductNo(Long productNo, String sortBy) { + return ReviewSearchDto.of(productNo, null, sortBy); + } + + // TODO: 추후 mypage 리뷰 관리에서 사용 예정 + public static ReviewSearchDto byMemberNo(Long memberNo, String sortBy) { + return ReviewSearchDto.of(null, memberNo, sortBy); + } +} diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java new file mode 100644 index 00000000..70be6d3d --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepository.java @@ -0,0 +1,4 @@ +package com.kernel360.review.repository; + +public interface ReviewRepository extends ReviewRepositoryJpa, ReviewRepositoryDsl { +} diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java new file mode 100644 index 00000000..45cbc0dd --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryDsl.java @@ -0,0 +1,10 @@ +package com.kernel360.review.repository; + +import com.kernel360.review.dto.ReviewSearchDto; +import com.kernel360.review.entity.Review; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; + +public interface ReviewRepositoryDsl { + Page findAllByCondition(ReviewSearchDto condition, Pageable pageable); +} diff --git a/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java new file mode 100644 index 00000000..1794fc7f --- /dev/null +++ b/module-api/src/main/java/com/kernel360/review/repository/ReviewRepositoryImpl.java @@ -0,0 +1,65 @@ +package com.kernel360.review.repository; + +import com.kernel360.review.dto.ReviewSearchDto; +import com.kernel360.review.entity.Review; +import com.querydsl.core.types.OrderSpecifier; +import com.querydsl.core.types.dsl.BooleanExpression; +import com.querydsl.jpa.impl.JPAQueryFactory; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; + +import java.util.List; + +import static com.kernel360.review.entity.QReview.review; + +@RequiredArgsConstructor +public class ReviewRepositoryImpl implements ReviewRepositoryDsl { + + private final JPAQueryFactory queryFactory; + + @Override + public Page findAllByCondition(ReviewSearchDto condition, Pageable pageable) { + List reviews = queryFactory + .select(review) + .from(review) + .where( + productNoEq(condition.productNo()), + memberNoEq(condition.memberNo())) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize()) + .orderBy(sort(condition.sortBy())) + .fetch(); + + Long totalCount = queryFactory + .select(review.count()) + .from(review) + .where( + productNoEq(condition.productNo()), + memberNoEq(condition.memberNo())) + .fetchOne(); + + return new PageImpl<>(reviews, pageable, totalCount); + } + + private BooleanExpression productNoEq(Long productNo) { + return productNo == null ? null : review.product.productNo.eq(productNo); + } + + private BooleanExpression memberNoEq(Long memberNo) { + return memberNo == null ? null : review.member.memberNo.eq(memberNo); + } + + private static OrderSpecifier sort(String sortBy) { + if ("topRated".equals(sortBy)) { + return review.starRating.desc(); + } + + if ("lowRated".equals(sortBy)) { + return review.starRating.asc(); + } + + return review.reviewNo.desc(); + } +} diff --git a/module-api/src/main/java/com/kernel360/review/service/ReviewService.java b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java index 86391415..2be8b398 100644 --- a/module-api/src/main/java/com/kernel360/review/service/ReviewService.java +++ b/module-api/src/main/java/com/kernel360/review/service/ReviewService.java @@ -3,15 +3,20 @@ import com.kernel360.exception.BusinessException; import com.kernel360.review.code.ReviewErrorCode; import com.kernel360.review.dto.ReviewDto; +import com.kernel360.review.dto.ReviewSearchDto; import com.kernel360.review.entity.Review; import com.kernel360.review.repository.ReviewRepository; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; -import java.util.List; +@Slf4j @Service @RequiredArgsConstructor public class ReviewService { @@ -21,15 +26,17 @@ public class ReviewService { private static final double MAX_STAR_RATING = 5.0; @Transactional(readOnly = true) - public List getReviewsByProduct(Long productNo) { + public Page getReviewsByProduct(Long productNo, String sortBy, Pageable pageable) { + log.info("제품 리뷰 목록 조회 -> product_no {}", productNo); + // TODO: 유효하지 않은 productNo 인 경우, custom error 보내기 - return reviewRepository.findAllByProduct_ProductNo(productNo) - .stream().map(ReviewDto::from) - .toList(); + return reviewRepository.findAllByCondition(ReviewSearchDto.byProductNo(productNo, sortBy), pageable) + .map(ReviewDto::from); } @Transactional(readOnly = true) public ReviewDto getReview(Long reviewNo) { + log.info("리뷰 단건 조회 -> review_no {}", reviewNo); return ReviewDto.from(reviewRepository.findByReviewNo(reviewNo)); } @@ -38,7 +45,16 @@ public ReviewDto getReview(Long reviewNo) { public Review createReview(ReviewDto reviewDto) { isValidStarRating(reviewDto.starRating()); - return reviewRepository.save(reviewDto.toEntity()); + Review review; + + try { + review = reviewRepository.saveAndFlush(reviewDto.toEntity()); + } catch (DataIntegrityViolationException e) { + throw new BusinessException(ReviewErrorCode.INVALID_REVIEW_WRITE_REQUEST); + } + + log.info("리뷰 등록 -> review_no {}", review.getReviewNo()); + return review; } @@ -46,12 +62,19 @@ public Review createReview(ReviewDto reviewDto) { public void updateReview(ReviewDto reviewDto) { isValidStarRating(reviewDto.starRating()); - reviewRepository.save(reviewDto.toEntity()); + try { + reviewRepository.saveAndFlush(reviewDto.toEntity()); + } catch (DataIntegrityViolationException e) { + throw new BusinessException(ReviewErrorCode.INVALID_REVIEW_WRITE_REQUEST); + } + + log.info("리뷰 수정 -> review_no {}", reviewDto.reviewNo()); } @Transactional public void deleteReview(Long reviewNo) { reviewRepository.deleteById(reviewNo); + log.info("리뷰 삭제 -> review_no {}", reviewNo); } private static void isValidStarRating(BigDecimal starRating) { diff --git a/module-api/src/main/resources/application-dev.yml b/module-api/src/main/resources/application-dev.yml index e6fea07d..87fd4b0d 100644 --- a/module-api/src/main/resources/application-dev.yml +++ b/module-api/src/main/resources/application-dev.yml @@ -66,6 +66,8 @@ constants: host-url: ${DEV_HOST_URL} password-reset-token: duration-minute: 5 + jwt: + expiring-minute: 60 aws: credentials: diff --git a/module-api/src/main/resources/application-local.yml b/module-api/src/main/resources/application-local.yml index 323c548b..c9ddd639 100644 --- a/module-api/src/main/resources/application-local.yml +++ b/module-api/src/main/resources/application-local.yml @@ -72,6 +72,9 @@ constants: host-url: "http://localhost:8080" password-reset-token: duration-minute: 5 + jwt: + expiring-minute: 60 + aws: credentials: diff --git a/module-api/src/main/resources/application-prod.yml b/module-api/src/main/resources/application-prod.yml index 983e1954..55eb3f00 100644 --- a/module-api/src/main/resources/application-prod.yml +++ b/module-api/src/main/resources/application-prod.yml @@ -66,6 +66,8 @@ constants: host-url: ${PROD_HOST_URL} password-reset-token: duration-minute: 5 + jwt: + expiring-minute: 3 # ${PROD_JWT_EXPIRATION} aws: credentials: diff --git a/module-api/src/main/resources/db/migration/V1.0.0__init.sql b/module-api/src/main/resources/db/migration/V1.0.0__init.sql index e5201d32..de376110 100644 --- a/module-api/src/main/resources/db/migration/V1.0.0__init.sql +++ b/module-api/src/main/resources/db/migration/V1.0.0__init.sql @@ -25,6 +25,7 @@ CREATE TABLE if not exists Auth created_by varchar NOT NULL, modified_at date NULL, modified_by varchar NULL + ); @@ -39,7 +40,8 @@ CREATE TABLE if not exists Member created_at DATE NOT NULL, created_by VARCHAR NOT NULL, modified_at DATE, - modified_by VARCHAR + modified_by VARCHAR, + account_type varchar ); diff --git a/module-api/src/main/resources/db/migration/V1.0.10__add_unique_constraint_review.sql b/module-api/src/main/resources/db/migration/V1.0.10__add_unique_constraint_review.sql new file mode 100644 index 00000000..57ef4e13 --- /dev/null +++ b/module-api/src/main/resources/db/migration/V1.0.10__add_unique_constraint_review.sql @@ -0,0 +1,4 @@ +ALTER TABLE REVIEW DROP CONSTRAINT IF EXISTS review_ukey; + +ALTER TABLE REVIEW + ADD CONSTRAINT review_ukey UNIQUE (MEMBER_NO, PRODUCT_NO); \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql b/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql index 6456f274..38c2d8a7 100644 --- a/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql +++ b/module-api/src/main/resources/db/migration/V1.0.2__add_member_view_with_secure.sql @@ -39,7 +39,8 @@ SELECT member_no, created_at, created_by, modified_at, - modified_by + modified_by, + account_type FROM member; @@ -51,10 +52,10 @@ CREATE RETURNS TRIGGER AS $$ BEGIN - INSERT INTO member (member_no, id, "password", email, gender, age, created_at, created_by, modified_at, modified_by) + INSERT INTO member (member_no, id, "password", email, gender, age, created_at, created_by, modified_at, modified_by, account_type) VALUES (nextval('member_member_no_seq'::regclass), NEW.id, pgp_sym_encrypt(NEW.password::TEXT, 'changeRequired'), pgp_sym_encrypt(NEW.email::TEXT, 'changeRequired'), NEW.gender, NEW.age, NEW.created_at, NEW.created_by, - NEW.modified_at, NEW.modified_by); + NEW.modified_at, NEW.modified_by, NEW.account_type); RETURN NEW; @@ -88,7 +89,8 @@ BEGIN created_at = NEW.created_at, created_by = NEW.created_by, modified_at = NEW.modified_at, - modified_by = NEW.modified_by + modified_by = NEW.modified_by, + account_type = NEW.account_type WHERE member_no = NEW.member_no; RETURN NEW; END; @@ -132,4 +134,4 @@ EXECUTE FUNCTION member_view_delete_trigger(); -- 테이블 member 권한 회수 설정 -REVOKE INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES ON member FROM wash_admin; \ No newline at end of file +REVOKE INSERT, UPDATE, DELETE, TRUNCATE ON member FROM wash_admin; \ No newline at end of file diff --git a/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql b/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql index 579e6241..07e58912 100644 --- a/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql +++ b/module-api/src/main/resources/db/migration/V1.0.9__add_clientIP_to_auth.sql @@ -1,2 +1,11 @@ -ALTER TABLE auth - ADD COLUMN client_ip varchar; \ No newline at end of file +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 + FROM information_schema.columns + WHERE table_name = 'auth' + AND column_name = 'client_ip' + ) THEN +ALTER TABLE auth ADD COLUMN client_ip VARCHAR; +END IF; +END $$; \ No newline at end of file diff --git a/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java b/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java index bfcc0340..0bb5827f 100644 --- a/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java +++ b/module-api/src/test/java/com/kernel360/main/controller/MainControllerTest.java @@ -80,19 +80,18 @@ class MainControllerTest extends ControllerTest { verify(mainService, times(1)).getBanner(); } - @Test - void 메인페이지_추천상품을_호출할때_200응답과_데이터가_잘보내지는지() throws Exception { + + void 메인페이지_추천상품을_호출할때_200응답과_데이터가_잘보내지는지_랜덤20개() throws Exception { // given List recommendProductsDtos = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) - .setNotNull("id") + .setNotNull("productNo") .setNotNull("imageSource") .setNotNull("alt") .setNotNull("productName") + .setNotNull("item") .sampleList(5); - Pageable pageable = PageRequest.of(0, 5); - Page page = new PageImpl<>(recommendProductsDtos, pageable, recommendProductsDtos.size()); - when(productService.getRecommendProducts(any(Pageable.class))).thenReturn(page); + when(productService.getRecommendProductsWithRandom()).thenReturn(recommendProductsDtos); // when & then mockMvc.perform(get("/recommend-products")) @@ -101,10 +100,11 @@ class MainControllerTest extends ControllerTest { .andExpect(jsonPath("$.status").value(200)) .andExpect(jsonPath("$.code").value("PMB001")) .andExpect(jsonPath("$.message").value("추천제품정보 조회 성공")) - .andExpect(jsonPath("$.value.content[*].id").exists()) + .andExpect(jsonPath("$.value.content[*].productNo").exists()) .andExpect(jsonPath("$.value.content[*].imageSource").exists()) .andExpect(jsonPath("$.value.content[*].alt").exists()) .andExpect(jsonPath("$.value.content[*].productName").exists()) + .andExpect(jsonPath("$.value.content[*].item").exists()) .andDo(document("recommend-products/get-recommend-products", getDocumentRequest(), getDocumentResponse(), @@ -113,37 +113,79 @@ class MainControllerTest extends ControllerTest { fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), fieldWithPath("message").type(JsonFieldType.STRING).description("응답메세지"), fieldWithPath("value").type(JsonFieldType.OBJECT).description("응답 본문의 루트 객체"), - fieldWithPath("value.content[].id").type(JsonFieldType.NUMBER).description("제품 ID"), + fieldWithPath("value.content[].productNo").type(JsonFieldType.NUMBER).description("제품 고유번호"), fieldWithPath("value.content[].imageSource").type(JsonFieldType.STRING).description("이미지 URL"), fieldWithPath("value.content[].alt").type(JsonFieldType.STRING).description("이미지 대체 텍스트"), fieldWithPath("value.content[].productName").type(JsonFieldType.STRING).description("제품명"), - fieldWithPath("value.pageable").type(JsonFieldType.OBJECT).description("페이지 정보"), - fieldWithPath("value.pageable.pageNumber").type(JsonFieldType.NUMBER).description("페이지 번호"), - fieldWithPath("value.pageable.pageSize").type(JsonFieldType.NUMBER).description("페이지 크기"), - fieldWithPath("value.pageable.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), - fieldWithPath("value.pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), - fieldWithPath("value.pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), - fieldWithPath("value.pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), - fieldWithPath("value.pageable.offset").type(JsonFieldType.NUMBER).description("페이지 오프셋"), - fieldWithPath("value.pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), - fieldWithPath("value.pageable.unpaged").type(JsonFieldType.BOOLEAN).description("비페이징 여부"), - fieldWithPath("value.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), - fieldWithPath("value.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), - fieldWithPath("value.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), - fieldWithPath("value.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), - fieldWithPath("value.totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), - fieldWithPath("value.totalElements").type(JsonFieldType.NUMBER).description("총 요소 수"), - fieldWithPath("value.last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"), - fieldWithPath("value.number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), - fieldWithPath("value.size").type(JsonFieldType.NUMBER).description("페이지 크기"), - fieldWithPath("value.numberOfElements").type(JsonFieldType.NUMBER).description("현재 페이지의 요소 수"), - fieldWithPath("value.first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"), - fieldWithPath("value.empty").type(JsonFieldType.BOOLEAN).description("비어 있는 페이지 여부") + fieldWithPath("value.content[].item").type(JsonFieldType.STRING).description("제품 분류") ) )); } + void 메인페이지_추천상품을_호출할때_200응답과_데이터가_잘보내지는지_페이저블() throws Exception { + // given + List recommendProductsDtos = fixtureMonkey.giveMeBuilder(RecommendProductsDto.class) + .setNotNull("productNo") + .setNotNull("imageSource") + .setNotNull("alt") + .setNotNull("productName") + .sampleList(5); + + Pageable pageable = PageRequest.of(0, 5); + Page page = new PageImpl<>(recommendProductsDtos, pageable, recommendProductsDtos.size()); + when(productService.getRecommendProducts(any(Pageable.class))).thenReturn(page); + +// when & then + mockMvc.perform(get("/recommend-products")) + .andExpect(status().isOk()) + .andExpect(content().contentType(MediaType.APPLICATION_JSON)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.code").value("PMB001")) + .andExpect(jsonPath("$.message").value("추천제품정보 조회 성공")) + .andExpect(jsonPath("$.value.content[*].productNo").exists()) + .andExpect(jsonPath("$.value.content[*].imageSource").exists()) + .andExpect(jsonPath("$.value.content[*].alt").exists()) + .andExpect(jsonPath("$.value.content[*].productName").exists()) + .andDo(document("recommend-products/get-recommend-products", + getDocumentRequest(), + getDocumentResponse(), + responseFields( + fieldWithPath("status").type(JsonFieldType.NUMBER).description("상태 코드"), + fieldWithPath("code").type(JsonFieldType.STRING).description("응답 코드"), + fieldWithPath("message").type(JsonFieldType.STRING).description("응답메세지"), + fieldWithPath("value").type(JsonFieldType.OBJECT).description("응답 본문의 루트 객체"), + fieldWithPath("value.content[].productNo").type(JsonFieldType.NUMBER).description("제품 고유번호"), + fieldWithPath("value.content[].imageSource").type(JsonFieldType.STRING).description("이미지 URL"), + fieldWithPath("value.content[].alt").type(JsonFieldType.STRING).description("이미지 대체 텍스트"), + fieldWithPath("value.content[].productName").type(JsonFieldType.STRING).description("제품명"), + fieldWithPath("value.pageable").type(JsonFieldType.OBJECT).description("페이지 정보"), + fieldWithPath("value.pageable.pageNumber").type(JsonFieldType.NUMBER).description("페이지 번호"), + fieldWithPath("value.pageable.pageSize").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.pageable.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), + fieldWithPath("value.pageable.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.pageable.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.pageable.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), + fieldWithPath("value.pageable.offset").type(JsonFieldType.NUMBER).description("페이지 오프셋"), + fieldWithPath("value.pageable.paged").type(JsonFieldType.BOOLEAN).description("페이징 여부"), + fieldWithPath("value.pageable.unpaged").type(JsonFieldType.BOOLEAN).description("비페이징 여부"), + fieldWithPath("value.sort").type(JsonFieldType.OBJECT).description("정렬 정보"), + fieldWithPath("value.sort.empty").type(JsonFieldType.BOOLEAN).description("정렬이 비어 있는지 여부"), + fieldWithPath("value.sort.sorted").type(JsonFieldType.BOOLEAN).description("정렬이 되었는지 여부"), + fieldWithPath("value.sort.unsorted").type(JsonFieldType.BOOLEAN).description("정렬이 되지 않았는지 여부"), + fieldWithPath("value.totalPages").type(JsonFieldType.NUMBER).description("총 페이지 수"), + fieldWithPath("value.totalElements").type(JsonFieldType.NUMBER).description("총 요소 수"), + fieldWithPath("value.last").type(JsonFieldType.BOOLEAN).description("마지막 페이지 여부"), + fieldWithPath("value.number").type(JsonFieldType.NUMBER).description("현재 페이지 번호"), + fieldWithPath("value.size").type(JsonFieldType.NUMBER).description("페이지 크기"), + fieldWithPath("value.numberOfElements").type(JsonFieldType.NUMBER).description("현재 페이지의 요소 수"), + fieldWithPath("value.first").type(JsonFieldType.BOOLEAN).description("첫 페이지 여부"), + fieldWithPath("value.empty").type(JsonFieldType.BOOLEAN).description("비어 있는 페이지 여부") + ) + )); + + } + @ParameterizedTest @EnumSource(value = Sort.class, names = {"VIEW_COUNT_PRODUCT_ORDER", "VIOLATION_PRODUCT_LIST", "RECENT_PRODUCT_ORDER"}) void 메인페이지_조회순으로_제품리스트_요청시_200응답과_데이터가_잘_반환되는지(Sort sortType) throws Exception { diff --git a/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java b/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java index 62732867..c8ae857b 100644 --- a/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java +++ b/module-api/src/test/java/com/kernel360/member/controller/MemberControllerTest.java @@ -131,8 +131,8 @@ class MemberControllerTest extends ControllerTest { "member/find-memberId", getDocumentRequest(), getDocumentResponse(), requestFields( fieldWithPath("email").type(JsonFieldType.STRING).description("회원가입시 입력한 이메일"), - fieldWithPath("authToken").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), - fieldWithPath("memberId").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), + fieldWithPath("token").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), + fieldWithPath("id").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), fieldWithPath("password").type(JsonFieldType.NULL).description("변경할 비밀번호(사용하지 않음)") ), responseFields( @@ -151,7 +151,7 @@ class MemberControllerTest extends ControllerTest { MemberDto memberDto = MemberDto.of("testMemberId", "testPassword001"); given(memberService.findByMemberId(credentialDto.memberId())).willReturn(memberDto); - given(findCredentialService.generatePasswordResetUri(memberDto)).willReturn("테스트 URI"); + given(findCredentialService.generatePasswordResetPageUri(memberDto)).willReturn("테스트 URI"); ObjectMapper objectMapper = new ObjectMapper(); String dtoAsString = objectMapper.writeValueAsString(credentialDto); @@ -164,8 +164,8 @@ class MemberControllerTest extends ControllerTest { "member/find-password", getDocumentRequest(), getDocumentResponse(), requestFields( fieldWithPath("email").type(JsonFieldType.NULL).description("회원가입시 입력한 이메일(사용하지 않음)"), - fieldWithPath("authToken").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), - fieldWithPath("memberId").type(JsonFieldType.STRING).description("회원 아이디"), + fieldWithPath("token").type(JsonFieldType.NULL).description("비밀번호 재설정 UUID 토큰(사용하지 않음)"), + fieldWithPath("id").type(JsonFieldType.STRING).description("회원 아이디"), fieldWithPath("password").type(JsonFieldType.NULL).description("변경할 비밀번호(사용하지 않음)") ), responseFields( @@ -182,21 +182,10 @@ class MemberControllerTest extends ControllerTest { String token = "testToken-1234-5678"; given(findCredentialService.getData(token)).willReturn("kernel360-testId"); - mockMvc.perform(MockMvcRequestBuilders.get("/member/reset-password?token="+token) + mockMvc.perform(MockMvcRequestBuilders.get("/member/find-password?token="+token) .contentType(MediaType.APPLICATION_JSON) .content(token)) - .andExpect(MockMvcResultMatchers.status().isFound()).andDo(document( - "member/get-reset-password", getDocumentRequest(), getDocumentResponse(), - queryParameters( - parameterWithName("token").description("비밀번호 재설정 UUID 토큰") - ), - responseFields( - fieldWithPath("status").type(JsonFieldType.NUMBER).description("HTTP 상태 코드"), - fieldWithPath("code").type(JsonFieldType.STRING).description("비즈니스 코드"), - fieldWithPath("message").type(JsonFieldType.STRING).description("상세 메시지"), - fieldWithPath("value").type(JsonFieldType.STRING).description("JSON BODY 데이터 - 비밀번호 재설정 토큰") - ) - )); + .andExpect(MockMvcResultMatchers.status().isFound()); } @Test @@ -215,9 +204,9 @@ class MemberControllerTest extends ControllerTest { .andDo(document( "member/post-reset-password", getDocumentRequest(), getDocumentResponse(), requestFields( - fieldWithPath("authToken").type(JsonFieldType.STRING).description("비밀번호 재설정 UUID 토큰"), + fieldWithPath("token").type(JsonFieldType.STRING).description("비밀번호 재설정 UUID 토큰"), fieldWithPath("email").type(JsonFieldType.NULL).description("회원가입시 입력한 이메일(사용하지 않음)"), - fieldWithPath("memberId").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), + fieldWithPath("id").type(JsonFieldType.NULL).description("회원 아이디(사용하지 않음)"), fieldWithPath("password").type(JsonFieldType.STRING).description("변경할 비밀번호") ), responseFields( diff --git a/module-api/src/test/java/com/kernel360/member/entity/MemberTest.java b/module-api/src/test/java/com/kernel360/member/entity/MemberTest.java index df6d9809..0b8e8130 100644 --- a/module-api/src/test/java/com/kernel360/member/entity/MemberTest.java +++ b/module-api/src/test/java/com/kernel360/member/entity/MemberTest.java @@ -1,5 +1,6 @@ package com.kernel360.member.entity; +import com.kernel360.member.enumset.AccountType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -25,7 +26,7 @@ class MemberTest { gender = 0; age = 3; - member = Member.of(memberNo, id, email, password, gender, age); + member = Member.of(memberNo, id, email, password, gender, age, AccountType.PLATFORM.name()); } @Test diff --git a/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java b/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java index 86803709..ab8986d9 100644 --- a/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java +++ b/module-api/src/test/java/com/kernel360/member/service/MemberServiceTest.java @@ -5,6 +5,7 @@ import com.kernel360.auth.service.AuthService; import com.kernel360.member.dto.MemberDto; import com.kernel360.member.entity.Member; +import com.kernel360.member.enumset.AccountType; import com.kernel360.member.repository.MemberRepository; import com.kernel360.utils.ConvertSHA256; import com.kernel360.utils.JWT; @@ -87,7 +88,7 @@ public void init() { MemberDto loginDto = MemberDto.of("test03", "1234qwer"); Member mockLoginEntity = Member.loginMember(loginDto.id(), loginDto.password()); Member mockEntity = Member.of(502L, loginDto.id(), "test03@naver.com", - "0eb9de69892882d54516e03e30098354a2e39cea36adab275b6300c737c942fd", 0, 0); + "0eb9de69892882d54516e03e30098354a2e39cea36adab275b6300c737c942fd", 0, 0, AccountType.PLATFORM.name()); String mockToken = "dummy_token"; /** stub **/ @@ -111,7 +112,7 @@ public void init() { void 토큰_발급_저장_테스트() { /** given **/ - Member memberEntity = Member.of(502L, "test03", null, null, 0, 0); + Member memberEntity = Member.of(502L, "test03", null, null, 0, 0, AccountType.PLATFORM.name()); String mockToken = "mockToken"; Auth auth = Auth.jwt(null, 502L, mockToken); MockHttpServletRequest request = new MockHttpServletRequest(); @@ -143,7 +144,7 @@ public void init() { /** given **/ String id = "test01"; - Member memberEntity = Member.of(51L, "test01", null, null, 0, 0); + Member memberEntity = Member.of(51L, "test01", null, null, 0, 0, AccountType.PLATFORM.name()); /** stub **/ when(memberRepository.findOneById(anyString())).thenReturn(memberEntity); @@ -182,7 +183,7 @@ public void init() { /** given **/ String email = "kernel360@kernel360.co.kr"; - Member memberEntity = Member.of(51L, "test01", "kernel360@kernel360.co.kr", null, 0, 0); + Member memberEntity = Member.of(51L, "test01", "kernel360@kernel360.co.kr", null, 0, 0, AccountType.PLATFORM.name()); /** stub **/ when(memberRepository.findOneByEmail(anyString())).thenReturn(memberEntity); diff --git a/module-common/src/main/java/com/kernel360/code/common/CommonErrorCode.java b/module-common/src/main/java/com/kernel360/code/common/CommonErrorCode.java index 07ce8680..75e1c04a 100644 --- a/module-common/src/main/java/com/kernel360/code/common/CommonErrorCode.java +++ b/module-common/src/main/java/com/kernel360/code/common/CommonErrorCode.java @@ -6,7 +6,12 @@ public enum CommonErrorCode implements ErrorCode { INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(), "E001", "Server Error"), FAIL_FILE_UPLOAD(HttpStatus.INTERNAL_SERVER_ERROR.value(), "E002", "파일 업로드 실패"), - INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST.value(), "E003", "유효하지 않은 파일 확장자"); + INVALID_FILE_EXTENSION(HttpStatus.BAD_REQUEST.value(), "E003", "유효하지 않은 파일 확장자"), + NOT_FOUND_RESOURCE(HttpStatus.NOT_FOUND.value(), "E004", "요청한 자원이 존재하지 않음"), + INVALID_REQUEST_HEADERS(HttpStatus.BAD_REQUEST.value(), "E005", "요청한 헤더가 존재하지 않음"), + INVALID_ARGUMENT(HttpStatus.BAD_REQUEST.value(), "E006", "요청 파라미터가 없거나 비어있거나, 요청 파라미터의 이름이 메서드 인수의 이름과 일치하지 않습니다"), + INVALID_HTTP_REQUEST_METHOD(HttpStatus.BAD_REQUEST.value(), "E007", "요청 URL 에서 지원하지 않는 HTTP Method 입니다."), + INVALID_REQUEST_PARAMETER(HttpStatus.BAD_REQUEST.value(), "E008", "요청한 파라미터가 존재하지 않음"); private final int status; private final String code; diff --git a/module-common/src/main/java/com/kernel360/code/jwt/JwtErrorCode.java b/module-common/src/main/java/com/kernel360/code/jwt/JwtErrorCode.java new file mode 100644 index 00000000..f41c6058 --- /dev/null +++ b/module-common/src/main/java/com/kernel360/code/jwt/JwtErrorCode.java @@ -0,0 +1,33 @@ +package com.kernel360.code.jwt; + +import com.kernel360.code.ErrorCode; +import org.springframework.http.HttpStatus; + +public enum JwtErrorCode implements ErrorCode { + FAILED_MALFORMED_JWT(HttpStatus.BAD_REQUEST.value(), "EJC001", "유효하지 않은 JWT 문자열 형식입니다."), + FAILED_SIGNATURE_JWT(HttpStatus.BAD_REQUEST.value(), "EJC001", "JWT 시그니처가 서버에서 계산한 시그니처와 일치하지 않습니다."); + private final int status; + private final String code; + private final String message; + + JwtErrorCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + @Override + public int getStatus() { + return status; + } + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} diff --git a/module-common/src/main/java/com/kernel360/handler/GlobalExceptionHandler.java b/module-common/src/main/java/com/kernel360/handler/GlobalExceptionHandler.java index b1f6a16b..def82f82 100644 --- a/module-common/src/main/java/com/kernel360/handler/GlobalExceptionHandler.java +++ b/module-common/src/main/java/com/kernel360/handler/GlobalExceptionHandler.java @@ -2,11 +2,17 @@ import com.kernel360.code.ErrorCode; import com.kernel360.code.common.CommonErrorCode; +import com.kernel360.code.jwt.JwtErrorCode; import com.kernel360.exception.BusinessException; import com.kernel360.response.ErrorResponse; +import io.jsonwebtoken.MalformedJwtException; +import io.jsonwebtoken.security.SignatureException; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; +import org.springframework.web.HttpRequestMethodNotSupportedException; +import org.springframework.web.bind.MissingRequestHeaderException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @@ -31,4 +37,67 @@ protected ResponseEntity handleException(Exception e) { return new ResponseEntity<>(response, HttpStatus.INTERNAL_SERVER_ERROR); } + + @ExceptionHandler(MalformedJwtException.class) + protected ResponseEntity handleMalformedJwtException(final MalformedJwtException e) { + log.error("handleMalformedJwtException", e); + + final ErrorResponse response = ErrorResponse.of(JwtErrorCode.FAILED_MALFORMED_JWT); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(SignatureException.class) + protected ResponseEntity handleJwtSignatureException(final SignatureException e) { + log.error("handleJwtSignatureException", e); + + final ErrorResponse response = ErrorResponse.of(JwtErrorCode.FAILED_SIGNATURE_JWT); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(NullPointerException.class) + protected ResponseEntity handleNullPointerException(final NullPointerException e) { + log.error("handleNullPointerException", e); + + final ErrorResponse response = ErrorResponse.of(CommonErrorCode.NOT_FOUND_RESOURCE); + + return new ResponseEntity<>(response, HttpStatus.NOT_FOUND); + } + + @ExceptionHandler(MissingRequestHeaderException.class) + protected ResponseEntity handleMissingHeaderException(final MissingRequestHeaderException e) { + log.error("handleMissingHeaderException", e); + + final ErrorResponse response = ErrorResponse.of(CommonErrorCode.INVALID_REQUEST_HEADERS); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(IllegalArgumentException.class) + protected ResponseEntity handleIllegalArgumentException(final IllegalArgumentException e){ + log.error("handleIllegalArgumentException",e); + + final ErrorResponse response = ErrorResponse.of(CommonErrorCode.INVALID_ARGUMENT); + + return new ResponseEntity<>(response,HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(HttpRequestMethodNotSupportedException.class) + protected ResponseEntity handleHttpRequestMethodNotSupportedException(final HttpRequestMethodNotSupportedException e){ + log.error("handleHttpRequestMethodNotSupportedException",e); + + final ErrorResponse response =ErrorResponse.of(CommonErrorCode.INVALID_HTTP_REQUEST_METHOD); + + return new ResponseEntity<>(response,HttpStatus.BAD_REQUEST); + } + + @ExceptionHandler(MissingServletRequestParameterException.class) + protected ResponseEntity handleMissingParameterException(final MissingServletRequestParameterException e) { + log.error("handleMissingParameterException", e); + + final ErrorResponse response = ErrorResponse.of(CommonErrorCode.INVALID_REQUEST_PARAMETER); + + return new ResponseEntity<>(response, HttpStatus.BAD_REQUEST); + } } diff --git a/module-common/src/main/java/com/kernel360/utils/JWT.java b/module-common/src/main/java/com/kernel360/utils/JWT.java index bdb72d04..7e3b28ed 100644 --- a/module-common/src/main/java/com/kernel360/utils/JWT.java +++ b/module-common/src/main/java/com/kernel360/utils/JWT.java @@ -3,6 +3,7 @@ import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.security.Key; @@ -14,16 +15,16 @@ @Component public class JWT { private static final Key SECRET_KEY = Keys.secretKeyFor(io.jsonwebtoken.SignatureAlgorithm.HS256); - //private static final long EXPIRATION_TIME = (long) 1000 * 60 * 15; //15분 - private static final long EXPIRATION_TIME = (long) 1000 * 60 * 3; //테스트를 위해 3분으로 조정 + @Value("${constants.jwt.expiring-minute}") + private long EXPIRATION_TIME; public String generateToken(String entityId) { return Jwts.builder() .setIssuer("washpedia") .setId(entityId) .setIssuedAt(new Date(System.currentTimeMillis())) - .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME)) + .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME * 60 * 1000)) .signWith(SECRET_KEY) .compact(); } diff --git a/module-domain/build.gradle b/module-domain/build.gradle index f25d8e86..e12b5d2a 100644 --- a/module-domain/build.gradle +++ b/module-domain/build.gradle @@ -27,6 +27,12 @@ dependencies { implementation group: 'org.mybatis.spring.boot', name: 'mybatis-spring-boot-starter', version: '3.0.3' compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' + + // querydsl + implementation 'com.querydsl:querydsl-jpa:5.0.0:jakarta' + annotationProcessor 'com.querydsl:querydsl-apt:5.0.0:jakarta' + annotationProcessor 'jakarta.annotation:jakarta.annotation-api' + annotationProcessor 'jakarta.persistence:jakarta.persistence-api' } tasks.named('test') { @@ -43,5 +49,17 @@ tasks.jar { tasks.register("prepareKotlinBuildScriptModel") {} +def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile; + +tasks.withType(JavaCompile) { + options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) +} + +sourceSets { + main.java.srcDirs += [ querydslDir ] +} +clean { + delete file(querydslDir) +} diff --git a/module-domain/src/main/java/com/kernel360/brand/entity/Brand.java b/module-domain/src/main/java/com/kernel360/brand/entity/Brand.java index 968c9df2..6b8bee1d 100644 --- a/module-domain/src/main/java/com/kernel360/brand/entity/Brand.java +++ b/module-domain/src/main/java/com/kernel360/brand/entity/Brand.java @@ -8,11 +8,14 @@ import jakarta.persistence.Id; import jakarta.persistence.SequenceGenerator; import jakarta.persistence.Table; +import lombok.AccessLevel; import lombok.Getter; +import lombok.NoArgsConstructor; @Getter @Entity @Table(name = "brand") +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class Brand extends BaseEntity { @Id @Column(name = "brand_no", nullable = false) @@ -31,4 +34,39 @@ public class Brand extends BaseEntity { @Column(name = "nation_name", length = Integer.MAX_VALUE) private String nationName; + + private Brand(String brandName, String companyName, String description, String nationName) { + this.brandName = brandName; + this.companyName = companyName; + this.description = description; + this.nationName = nationName; + } + + public static Brand toEntity(String brandName, String companyName, String description, String nationName) { + + return new Brand(brandName, companyName, description, nationName); + } + + public void updateDescription(String description) { + this.description = description; + } + + public void updateBrandName(String brandName) { + this.brandName = brandName; + } + + public void updateBrandCompanyName(String companyName) { + this.companyName = companyName; + } + + public void updateBrandNationName(String nationName) { + this.nationName = nationName; + } + + public void updateAll(String brandName, String companyName, String description, String nationName) { + this.brandName = brandName; + this.companyName = companyName; + this.description = description; + this.nationName = nationName; + } } \ No newline at end of file diff --git a/module-domain/src/main/java/com/kernel360/brand/repository/BrandRepository.java b/module-domain/src/main/java/com/kernel360/brand/repository/BrandRepository.java index 67c4a2fd..eda79edd 100644 --- a/module-domain/src/main/java/com/kernel360/brand/repository/BrandRepository.java +++ b/module-domain/src/main/java/com/kernel360/brand/repository/BrandRepository.java @@ -2,6 +2,7 @@ import com.kernel360.brand.entity.Brand; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; import org.springframework.data.repository.query.Param; @@ -9,7 +10,10 @@ @Repository public interface BrandRepository extends JpaRepository { - @Query("SELECT b FROM Brand b where b.companyName like :companyName") List findByCompanyName(@Param("companyName") String companyName); + + Optional findBrandByBrandName(String brandName); + + Optional findBrandByBrandNo(Long brandNo); } diff --git a/module-domain/src/main/java/com/kernel360/config/QuerydslConfig.java b/module-domain/src/main/java/com/kernel360/config/QuerydslConfig.java new file mode 100644 index 00000000..5e780bfb --- /dev/null +++ b/module-domain/src/main/java/com/kernel360/config/QuerydslConfig.java @@ -0,0 +1,15 @@ +package com.kernel360.config; + +import com.querydsl.jpa.impl.JPAQueryFactory; +import jakarta.persistence.EntityManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class QuerydslConfig { + + @Bean + public JPAQueryFactory queryFactory(EntityManager entityManager) { + return new JPAQueryFactory(entityManager); + } +} \ No newline at end of file diff --git a/module-domain/src/main/java/com/kernel360/likes/entity/Like.java b/module-domain/src/main/java/com/kernel360/likes/entity/Like.java index 22292911..81c3a3ca 100644 --- a/module-domain/src/main/java/com/kernel360/likes/entity/Like.java +++ b/module-domain/src/main/java/com/kernel360/likes/entity/Like.java @@ -9,7 +9,9 @@ @Getter @Entity -@Table(name = "likes") +@Table(name = "likes", uniqueConstraints = { + @UniqueConstraint(columnNames = {"member_no", "product_no"}) +}) @NoArgsConstructor(access = AccessLevel.PROTECTED) public class Like extends BaseEntity { @Id diff --git a/module-domain/src/main/java/com/kernel360/member/entity/Member.java b/module-domain/src/main/java/com/kernel360/member/entity/Member.java index 0210405c..27e154e7 100644 --- a/module-domain/src/main/java/com/kernel360/member/entity/Member.java +++ b/module-domain/src/main/java/com/kernel360/member/entity/Member.java @@ -41,9 +41,12 @@ public class Member extends BaseEntity { @Column(name = "age") private int age; - public static Member of(Long memberNo, String id, String email, String password, int gender, int age) { + @Column(name = "account_type") + private String accountType; - return new Member(memberNo, id, email, password, gender, age); + public static Member of(Long memberNo, String id, String email, String password, int gender, int age, String accountType) { + + return new Member(memberNo, id, email, password, gender, age, accountType); } /** @@ -55,7 +58,8 @@ private Member( String email, String password, int gender, - int age + int age, + String accountType ) { this.memberNo = memberNo; this.id = id; @@ -63,25 +67,27 @@ private Member( this.password = password; this.gender = gender; this.age = age; + this.accountType = accountType; } /** * joinMember **/ - public static Member createJoinMember(String id, String email, String password, int gender, int age) { + public static Member createJoinMember(String id, String email, String password, int gender, int age, String accountType) { - return new Member(id, email, password, gender, age); + return new Member(id, email, password, gender, age, accountType); } /** * joinMember Binding **/ - private Member(String id, String email, String password, int gender, int age) { + private Member(String id, String email, String password, int gender, int age, String accountType) { this.id = id; this.email = email; this.password = password; this.gender = gender; this.age = age; + this.accountType = accountType; } /** @@ -112,11 +118,6 @@ public void updateCarInfo(CarInfo carInfo) { this.carInfo = carInfo; } - public static Member createForKakao(String id, String email, String password, int gender, int age) { - - return new Member(id, email, password, gender, age); - } - public void updateFromInfo(int gender, int age) { this.gender = gender; this.age = age; diff --git a/module-domain/src/main/java/com/kernel360/member/entity/WithdrawMember.java b/module-domain/src/main/java/com/kernel360/member/entity/WithdrawMember.java index 25f4c976..9ee4dde3 100644 --- a/module-domain/src/main/java/com/kernel360/member/entity/WithdrawMember.java +++ b/module-domain/src/main/java/com/kernel360/member/entity/WithdrawMember.java @@ -29,9 +29,9 @@ public class WithdrawMember extends BaseEntity { private String ip; - public static WithdrawMember of(Long memberNo, String id, String email, String ip) { + public static WithdrawMember of(Member member) { - return new WithdrawMember(memberNo, id, email, ip); + return new WithdrawMember(member.getMemberNo(), member.getId(), member.getEmail(), null); //IP정보를 저장한다면 파라메터를 추가해야 } private WithdrawMember( diff --git a/module-domain/src/main/java/com/kernel360/member/repository/MemberRepository.java b/module-domain/src/main/java/com/kernel360/member/repository/MemberRepository.java index 956d8a57..b01e4481 100644 --- a/module-domain/src/main/java/com/kernel360/member/repository/MemberRepository.java +++ b/module-domain/src/main/java/com/kernel360/member/repository/MemberRepository.java @@ -3,6 +3,7 @@ import com.kernel360.member.entity.Member; import jakarta.persistence.Id; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository { @@ -11,7 +12,12 @@ public interface MemberRepository extends JpaRepository { Member findOneById(String id); + @Query("SELECT m FROM Member m WHERE m.id = :id AND m.accountType = 'PLATFORM'") + Member findOneByIdForAccountTypeByPlatform(String id); + Member findOneByEmail(String email); + @Query("SELECT m FROM Member m WHERE m.email = :email AND m.accountType = 'PLATFORM'") + Member findOneByEmailForAccountTypeByPlatform(String email); } diff --git a/module-domain/src/main/java/com/kernel360/product/repository/ProductRepository.java b/module-domain/src/main/java/com/kernel360/product/repository/ProductRepository.java index 024bfff9..c7ec0071 100644 --- a/module-domain/src/main/java/com/kernel360/product/repository/ProductRepository.java +++ b/module-domain/src/main/java/com/kernel360/product/repository/ProductRepository.java @@ -2,6 +2,7 @@ import com.kernel360.product.entity.Product; +import java.util.List; import java.util.Optional; import com.kernel360.product.entity.SafetyStatus; @@ -20,6 +21,9 @@ public interface ProductRepository extends JpaRepository { Page findTop5ByOrderByProductNameDesc(Pageable pageable); + @Query(value = "SELECT * FROM Product p ORDER BY RANDOM() LIMIT 20", nativeQuery = true) + List getRecommendProductsWithRandom(); + Page findAllBySafetyStatusEquals(SafetyStatus safetyStatus, Pageable pageable); Page findAllByOrderByCreatedAtDesc(Pageable pageable); @@ -73,4 +77,7 @@ Optional findProductByProductNameAndCompanyName(@Param("productName") S Page findByProductWithKeywordAndSafetyStatus(@Param("keyword") String keyword, @Param("safetyStatus") SafetyStatus safetyStatus, Pageable pageable); + + Page findByProductNameContaining(String keyword, Pageable pageable); + } diff --git a/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepository.java b/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java similarity index 54% rename from module-domain/src/main/java/com/kernel360/review/repository/ReviewRepository.java rename to module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java index 556939e0..23628bcd 100644 --- a/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepository.java +++ b/module-domain/src/main/java/com/kernel360/review/repository/ReviewRepositoryJpa.java @@ -3,10 +3,6 @@ import com.kernel360.review.entity.Review; import org.springframework.data.jpa.repository.JpaRepository; -import java.util.List; - -public interface ReviewRepository extends JpaRepository { - List findAllByProduct_ProductNo(Long productNo); - +public interface ReviewRepositoryJpa extends JpaRepository { Review findByReviewNo(Long reviewNo); }