diff --git a/.gitignore b/.gitignore index 30b1cd0..993ce84 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ build/ ## 서버 설정 정보 ## !**/src/main/resources/application.yml +!**/src/main/resources/application-dev.yml ### STS ### .apt_generated diff --git a/build.gradle b/build.gradle index a02cd69..cccefc6 100644 --- a/build.gradle +++ b/build.gradle @@ -39,6 +39,7 @@ dependencies { // security implementation 'org.springframework.boot:spring-boot-starter-security' implementation "io.jsonwebtoken:jjwt:0.9.1" + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // test testImplementation 'org.springframework.boot:spring-boot-starter-test' @@ -47,6 +48,8 @@ dependencies { implementation 'com.sun.xml.bind:jaxb-impl:4.0.1' implementation 'com.sun.xml.bind:jaxb-core:4.0.1' implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359' + + implementation 'commons-io:commons-io:2.6' } tasks.named('test') { diff --git a/src/main/java/com/sideProject/PlanIT/PlanItApplication.java b/src/main/java/com/sideProject/PlanIT/PlanItApplication.java index 38b537b..4243417 100644 --- a/src/main/java/com/sideProject/PlanIT/PlanItApplication.java +++ b/src/main/java/com/sideProject/PlanIT/PlanItApplication.java @@ -2,7 +2,6 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.redis.repository.configuration.EnableRedisRepositories; @SpringBootApplication public class PlanItApplication { diff --git a/src/main/java/com/sideProject/PlanIT/common/modules/FileHandler.java b/src/main/java/com/sideProject/PlanIT/common/modules/FileHandler.java index 248b9d0..9ac9375 100644 --- a/src/main/java/com/sideProject/PlanIT/common/modules/FileHandler.java +++ b/src/main/java/com/sideProject/PlanIT/common/modules/FileHandler.java @@ -1,13 +1,19 @@ package com.sideProject.PlanIT.common.modules; +import com.sideProject.PlanIT.common.response.CustomException; +import com.sideProject.PlanIT.common.response.ErrorCode; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.io.IOUtils; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import org.springframework.web.multipart.MultipartFile; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; @Component +@Slf4j public class FileHandler { @Value("${spring.fileStorage.dir}") private String fileStorageDir; @@ -28,6 +34,20 @@ public String saveFile(MultipartFile file) { } } + public byte[] loadImage(String path) throws IOException { + String allPath = fileStorageDir +"images"+ path; + log.info(allPath); + File imageFile = new File(allPath); + if (imageFile.exists()) { + FileInputStream fileInputStream = new FileInputStream(imageFile); + byte[] imageBytes = IOUtils.toByteArray(fileInputStream); + fileInputStream.close(); + return imageBytes; + } else{ + throw new CustomException("이미지가 존재하지 않습니다.", ErrorCode.IMAGE_NOT_FOUND); + } + } + public MultipartFile sendFile(String dir) { //todo: 파일 전송 return null; diff --git a/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java b/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java index e35fd53..d330c03 100644 --- a/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java +++ b/src/main/java/com/sideProject/PlanIT/common/response/ErrorCode.java @@ -12,6 +12,11 @@ public enum ErrorCode { ALREADY_APPROVE_PROGRAM(400, "이미 등록된 프로그램입니다."), ALREADY_EXIST_EMAIL(400, "이미 존재하는 이메일입니다."), INVALID_PASSWORD(400, "비밀번호가 틀렸습니다."), + NOT_PT(400, "PT이용권이 아닙니다."), + NOT_YOUR_TRAINER(400, "예약 가능한 트레이너가 아닙니다."), + ALREADY_RESERVATION(400, "이미 예약 되어있습니다."), + + //401 INVALID_ACCESS_TOKEN(401, "ACCESS TOKEN 오류"), @@ -26,6 +31,9 @@ public enum ErrorCode { REGISTRATION_NOT_FOUND(404,"등록을 찾을 수 없습니다"), MEMBER_NOT_FOUND(404,"회원을 찾을 수 없습니다"), EMPLOYEE_NOT_FOUND(404,"직원을 찾을 수 없습니다"), + IMAGE_NOT_FOUND(404,"이미지를 찾을 수 없습니다"), + RESERVATION_NOT_FOUND(404,"등록되지 않은 예약입니다."), + FILE_NOT_FOUND(404,"파일을 찾을 수 없습니다"), NOT_SUSPEND_PROGRAM(422, "일시정지 요청이 거부되었습니다."), SUSPEND_REQUEST_DENIED(422, "일시정지 요청이 거부되었습니다."); diff --git a/src/main/java/com/sideProject/PlanIT/config/SecurityConfig.java b/src/main/java/com/sideProject/PlanIT/config/SecurityConfig.java index 629f9e3..e72a4e9 100644 --- a/src/main/java/com/sideProject/PlanIT/config/SecurityConfig.java +++ b/src/main/java/com/sideProject/PlanIT/config/SecurityConfig.java @@ -16,19 +16,16 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; -import org.springframework.web.cors.UrlBasedCorsConfigurationSource; -import java.util.Arrays; import java.util.Collections; import java.util.List; -import static org.springframework.security.config.Customizer.withDefaults; - @EnableWebSecurity @Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig { + @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); @@ -62,12 +59,13 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { .exceptionHandling((exceptionHandling) -> exceptionHandling.authenticationEntryPoint(jwtAuthenticationEntryPoint)) .authorizeHttpRequests((authorizeRequests) -> authorizeRequests - .requestMatchers("/member/signin", "/member/signup", "/refresh").permitAll() + .requestMatchers("/member/signin", "/member/signup", "member/refresh").permitAll() .requestMatchers("/admin/**").hasAnyAuthority("ADMIN") .anyRequest().authenticated() // .anyRequest().permitAll() ) .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class) + .build(); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/file/controller/FileController.java b/src/main/java/com/sideProject/PlanIT/domain/file/controller/FileController.java new file mode 100644 index 0000000..14ca0c2 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/file/controller/FileController.java @@ -0,0 +1,44 @@ +package com.sideProject.PlanIT.domain.file.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.file.service.FileService; +import com.sideProject.PlanIT.domain.post.dto.request.NoticeRequestDto; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.core.io.Resource; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.io.IOException; + +@RestController +@RequestMapping("/file") +public class FileController { + @Autowired + FileService fileService; + + @PostMapping + public ApiResponse saveFile(@ModelAttribute NoticeRequestDto noticeRequestDto) { + return ApiResponse.ok(fileService.saveFile(noticeRequestDto.getImage())); + } + @GetMapping("/{file_name:.+}") + public ResponseEntity sendFile(@PathVariable String file_name, HttpServletRequest request) { + Resource resource = fileService.sendFile(file_name); + + String contentType = null; + try { + contentType = request.getServletContext().getMimeType(resource.getFile().getAbsolutePath()); + } catch (IOException e) { + e.printStackTrace(); + } + + if (contentType == null) { + contentType = "application/octet-stream"; + } + + return ResponseEntity.ok() + .contentType(MediaType.parseMediaType(contentType)) + .body(resource); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/file/service/FileService.java b/src/main/java/com/sideProject/PlanIT/domain/file/service/FileService.java new file mode 100644 index 0000000..8fc7925 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/file/service/FileService.java @@ -0,0 +1,16 @@ +package com.sideProject.PlanIT.domain.file.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.util.UUID; + +public interface FileService { + public String saveFile(MultipartFile file); + public Resource sendFile(String fileName); +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/file/service/FileServiceImpl.java b/src/main/java/com/sideProject/PlanIT/domain/file/service/FileServiceImpl.java new file mode 100644 index 0000000..816c2f8 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/file/service/FileServiceImpl.java @@ -0,0 +1,54 @@ +package com.sideProject.PlanIT.domain.file.service; + +import com.sideProject.PlanIT.common.response.CustomException; +import com.sideProject.PlanIT.common.response.ErrorCode; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.IOException; +import java.net.MalformedURLException; +import java.util.UUID; + +@Service +public class FileServiceImpl implements FileService{ + @Value("${spring.fileStorage.dir}") + private String fileStorageDir; + + @Override + public String saveFile(MultipartFile file) { + if (file == null || file.isEmpty()) { + return null; + } + try { + UUID uuid = UUID.randomUUID(); + String fileName = uuid + file.getOriginalFilename(); + File dest = new File(fileStorageDir + File.separator + fileName); + file.transferTo(dest); + + return fileName; + } catch (IOException e) { + return "이미지 업로드 오류 발생"; + } + } + + @Override + public Resource sendFile(String fileName) { + try { + File file = new File(fileStorageDir + File.separator + fileName); + + Resource resource = new FileSystemResource(file); + + if (resource.exists() || resource.isReadable()) { + return resource; + } else { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + } catch (Exception e) { + throw new CustomException(ErrorCode.FILE_NOT_FOUND); + } + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerAdminController.java b/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerAdminController.java new file mode 100644 index 0000000..8d3e451 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerAdminController.java @@ -0,0 +1,37 @@ +package com.sideProject.PlanIT.domain.post.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.post.dto.request.BannerRequestDto; +import com.sideProject.PlanIT.domain.post.dto.response.BannerResponseDto; +import com.sideProject.PlanIT.domain.post.service.BannerService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin/banner") +@RequiredArgsConstructor +public class BannerAdminController { + private final BannerService bannerService; + + @PostMapping + public ApiResponse createBanner(@ModelAttribute BannerRequestDto bannerRequestDto) { + return ApiResponse.ok(bannerService.createBanner(bannerRequestDto)); + } + + @PutMapping("/{banner_id}") + public ApiResponse editBanner(@PathVariable Long banner_id, @ModelAttribute BannerRequestDto bannerRequestDto) { + return ApiResponse.ok(bannerService.editBanner(banner_id, bannerRequestDto)); + } + + @DeleteMapping("/{banner_id}") + public ApiResponse deleteBanner(@PathVariable Long banner_id) { + return ApiResponse.ok(bannerService.deleteBanner(banner_id)); + } + + @GetMapping + public ApiResponse> findAllBanners() { + return ApiResponse.ok(bannerService.findAllBanners()); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerController.java b/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerController.java index c022063..71c8d90 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerController.java +++ b/src/main/java/com/sideProject/PlanIT/domain/post/controller/BannerController.java @@ -1,48 +1,31 @@ package com.sideProject.PlanIT.domain.post.controller; import com.sideProject.PlanIT.common.response.ApiResponse; -import com.sideProject.PlanIT.domain.post.dto.request.BannerRequestDto; import com.sideProject.PlanIT.domain.post.dto.response.BannerResponseDto; import com.sideProject.PlanIT.domain.post.service.BannerService; -import lombok.AllArgsConstructor; -import org.springframework.web.bind.annotation.*; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; //todo: Banner, Notice 둘 다 Edit 발생 시 기존 첨부파일 삭제 후 다시 저장 @RestController -@AllArgsConstructor +@RequiredArgsConstructor +@RequestMapping("/banner") public class BannerController { private final BannerService bannerService; - @PostMapping("/admin/banner") - public ApiResponse createBanner(@ModelAttribute BannerRequestDto bannerRequestDto) { - return ApiResponse.ok(bannerService.createBanner(bannerRequestDto)); - } - - @PutMapping("admin/banner/{banner_id}") - public ApiResponse editBanner(@PathVariable Long banner_id, @ModelAttribute BannerRequestDto bannerRequestDto) { - return ApiResponse.ok(bannerService.editBanner(banner_id, bannerRequestDto)); - } - - @DeleteMapping("/admin/banner/{banner_id}") - public ApiResponse deleteBanner(@PathVariable Long banner_id) { - return ApiResponse.ok(bannerService.deleteBanner(banner_id)); - } - - @GetMapping("/admin/banner") - public ApiResponse> findAllBanners() { - return ApiResponse.ok(bannerService.findAllBanners()); - } - - @GetMapping("/banner") + @GetMapping public ApiResponse> findAllBannersInTime() { return ApiResponse.ok(bannerService.findAllBannersInTime()); } - @GetMapping("/banner/{banner_id}") + @GetMapping("/{banner_id}") public ApiResponse findBanner(@PathVariable Long banner_id) { return ApiResponse.ok(bannerService.findBanner(banner_id)); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeAdminController.java b/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeAdminController.java new file mode 100644 index 0000000..c073d88 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeAdminController.java @@ -0,0 +1,37 @@ +package com.sideProject.PlanIT.domain.post.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.post.dto.request.NoticeRequestDto; +import com.sideProject.PlanIT.domain.post.dto.response.NoticeResponseDto; +import com.sideProject.PlanIT.domain.post.service.NoticeService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/admin/notice") +@RequiredArgsConstructor +public class NoticeAdminController { + private final NoticeService noticeService; + + @PostMapping + public ApiResponse createNotice(@ModelAttribute NoticeRequestDto noticeRequestDto) { + return ApiResponse.ok(noticeService.createNotice(noticeRequestDto)); + } + + @PutMapping("/{notice_id}") + public ApiResponse editNotice(@PathVariable Long notice_id , @ModelAttribute NoticeRequestDto noticeRequestDto) { + return ApiResponse.ok(noticeService.editNotice(notice_id, noticeRequestDto)); + } + + @DeleteMapping("/{notice_id}") + public ApiResponse deleteNotice(@PathVariable Long notice_id) { + return ApiResponse.ok(noticeService.deleteNotice(notice_id)); + } + + @GetMapping + public ApiResponse> findAllNotices() { + return ApiResponse.ok(noticeService.findAllNotices()); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeController.java b/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeController.java index 383cd21..5590407 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeController.java +++ b/src/main/java/com/sideProject/PlanIT/domain/post/controller/NoticeController.java @@ -2,45 +2,28 @@ import com.sideProject.PlanIT.common.response.ApiResponse; -import com.sideProject.PlanIT.domain.post.dto.request.NoticeRequestDto; import com.sideProject.PlanIT.domain.post.dto.response.NoticeResponseDto; import com.sideProject.PlanIT.domain.post.service.NoticeService; -import lombok.AllArgsConstructor; -import org.springframework.web.bind.annotation.*; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController -@AllArgsConstructor +@RequiredArgsConstructor +@RequestMapping("/notice") public class NoticeController { private final NoticeService noticeService; - @PostMapping("/admin/notice") - public ApiResponse createNotice(@ModelAttribute NoticeRequestDto noticeRequestDto) { - return ApiResponse.ok(noticeService.createNotice(noticeRequestDto)); - } - - @PutMapping("/admin/notice/{notice_id}") - public ApiResponse editNotice(@PathVariable Long notice_id , @ModelAttribute NoticeRequestDto noticeRequestDto) { - return ApiResponse.ok(noticeService.editNotice(notice_id, noticeRequestDto)); - } - - @DeleteMapping("/admin/notice/{notice_id}") - public ApiResponse deleteNotice(@PathVariable Long notice_id) { - return ApiResponse.ok(noticeService.deleteNotice(notice_id)); - } - - @GetMapping("/admin/notice") - public ApiResponse> findAllNotices() { - return ApiResponse.ok(noticeService.findAllNotices()); - } - - @GetMapping("/notice") + @GetMapping public ApiResponse> findAllNoticesInTime() { return ApiResponse.ok(noticeService.findAllNoticesInTime()); } - @GetMapping("/notice/{notice_id}") + @GetMapping("/{notice_id}") public ApiResponse findNotice(@PathVariable Long notice_id) { return ApiResponse.ok(noticeService.findNotice(notice_id)); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductAdminController.java b/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductAdminController.java new file mode 100644 index 0000000..77a93aa --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductAdminController.java @@ -0,0 +1,25 @@ +package com.sideProject.PlanIT.domain.product.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.product.dto.request.ProductRequestDto; +import com.sideProject.PlanIT.domain.product.entity.Product; +import com.sideProject.PlanIT.domain.product.service.ProductService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/admin/product") +@RequiredArgsConstructor +public class ProductAdminController { + private final ProductService productService; + + @PostMapping + public ApiResponse createProduct(@RequestBody ProductRequestDto productRequestDto) { + return ApiResponse.ok(productService.createProduct(productRequestDto)); + } + + @DeleteMapping("/{product_id}") + public ApiResponse deleteProduct(@PathVariable(name = "product_id") Long product_id) { + return ApiResponse.ok(productService.deleteProduct(product_id)); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductController.java b/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductController.java index 5c45cf7..1328a29 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductController.java +++ b/src/main/java/com/sideProject/PlanIT/domain/product/controller/ProductController.java @@ -2,11 +2,9 @@ import com.sideProject.PlanIT.common.response.ApiResponse; import com.sideProject.PlanIT.domain.product.controller.enums.ProductSearchOption; -import com.sideProject.PlanIT.domain.product.dto.request.ProductRequestDto; import com.sideProject.PlanIT.domain.product.dto.response.ProductResponseDto; -import com.sideProject.PlanIT.domain.product.entity.Product; import com.sideProject.PlanIT.domain.product.service.ProductService; -import lombok.AllArgsConstructor; +import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -14,27 +12,13 @@ import org.springframework.web.bind.annotation.*; @RestController -@AllArgsConstructor +@RequiredArgsConstructor +@RequestMapping("/product") public class ProductController { private final ProductService productService; - @PostMapping("/admin/product") - public ApiResponse createProduct(@RequestBody ProductRequestDto productRequestDto) { - return ApiResponse.ok(productService.createProduct(productRequestDto)); - } - -// @PutMapping("/admin/product/{product_id}") -// public ApiResponse editProduct(@PathVariable Long product_id, @RequestBody ProductRequestDto productRequestDto) { -// return ApiResponse.ok(productService.editProduct(product_id, productRequestDto)); -// } - - @DeleteMapping("/admin/product/{product_id}") - public ApiResponse deleteProduct(@PathVariable(name = "product_id") Long product_id) { - return ApiResponse.ok(productService.deleteProduct(product_id)); - } - - @GetMapping("/product") + @GetMapping public ApiResponse> findAllProducts( @RequestParam(value = "option", required = false, defaultValue = "SELLING") ProductSearchOption option, @PageableDefault(size = 10,sort = "id",direction = Sort.Direction.DESC) Pageable pageable @@ -42,7 +26,7 @@ public ApiResponse> findAllProducts( return ApiResponse.ok(productService.findAllProducts(option,pageable)); } - @GetMapping("/product/{product_id}") + @GetMapping("/{product_id}") public ApiResponse findProduct(@PathVariable(name = "product_id") Long product_id) { return ApiResponse.ok(productService.findProduct(product_id)); } diff --git a/src/main/java/com/sideProject/PlanIT/domain/program/entity/Program.java b/src/main/java/com/sideProject/PlanIT/domain/program/entity/Program.java index 6e63566..8a42d8d 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/program/entity/Program.java +++ b/src/main/java/com/sideProject/PlanIT/domain/program/entity/Program.java @@ -1,7 +1,10 @@ package com.sideProject.PlanIT.domain.program.entity; import com.sideProject.PlanIT.common.baseentity.BaseEntity; +import com.sideProject.PlanIT.common.response.CustomException; +import com.sideProject.PlanIT.common.response.ErrorCode; import com.sideProject.PlanIT.domain.product.entity.Product; +import com.sideProject.PlanIT.domain.product.entity.enums.ProductType; import com.sideProject.PlanIT.domain.program.entity.enums.ProgramStatus; import com.sideProject.PlanIT.domain.user.entity.Employee; import com.sideProject.PlanIT.domain.user.entity.Member; @@ -14,6 +17,8 @@ import java.time.LocalDate; +import static com.sideProject.PlanIT.domain.program.entity.enums.ProgramStatus.EXPIRED; + @Entity @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @@ -117,4 +122,24 @@ public void resumeProgram(LocalDate resumeAt,LocalDate endAt) { this.endAt = endAt; this.status = ProgramStatus.IN_PROGRESS; } + + public void reservation() { + if(this.remainedNumber == 0) { + throw new CustomException(this.id + "의 남은 횟수가 없습니다", ErrorCode.EMPLOYEE_NOT_FOUND); + }else if(this.remainedNumber ==1 ){ + this.status = ProgramStatus.EXPIRED; + } + this.remainedNumber--; + } + + //todo : 에약 취소 경우 추가 + public void cancelReservation() { + if(this.product.getType() != ProductType.PT) { + throw new CustomException(this.id + "의 남은 횟수가 없습니다", ErrorCode.EMPLOYEE_NOT_FOUND); + } + if(this.remainedNumber+1 > this.product.getNumber()) { + throw new CustomException(this.id + "의 남은 횟수가 없습니다", ErrorCode.EMPLOYEE_NOT_FOUND); + } + this.remainedNumber++; + } } diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/controller/ReservationController.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/controller/ReservationController.java new file mode 100644 index 0000000..12f48c8 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/controller/ReservationController.java @@ -0,0 +1,87 @@ +package com.sideProject.PlanIT.domain.reservation.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.reservation.dto.reqeust.ChangeReservationRequest; +import com.sideProject.PlanIT.domain.reservation.dto.reqeust.ReservationRequest; +import com.sideProject.PlanIT.domain.reservation.dto.response.ReservationResponse; +import com.sideProject.PlanIT.domain.reservation.service.ReservationService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.format.annotation.DateTimeFormat; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/reservation") +public class ReservationController { + private final ReservationService reservationService; + + @PutMapping("/change/{employeeId}") + public ApiResponse changeAvailability( + Principal principal, + @RequestBody ChangeReservationRequest request, + @PathVariable("employeeId") Long employeeId + ) { + return ApiResponse.ok( + reservationService.changeAvailability( + request.getReservedTimes(), + employeeId, + Long.valueOf(principal.getName()) + ) + ); + } + + @PostMapping("/{reservationId}") + public ApiResponse reservation( + Principal principal, + @PathVariable("reservationId") Long reservationId, + @RequestBody ReservationRequest request + ) { + LocalDateTime now = LocalDateTime.now(); + return ApiResponse.ok( + reservationService.reservation( + reservationId, + Long.valueOf(principal.getName()), + request.getProgramId() + ) + ); + } + + @GetMapping("") + public ApiResponse>> findReservation( + @RequestParam(value = "date", required = false) + @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date, + Principal principal + ) { + if (date == null) { + date = LocalDate.now(); // 파라미터가 없을 경우 기본값으로 오늘 날짜를 사용 + } + System.out.println(principal.getName()); + return ApiResponse.ok( + reservationService.findReservationForWeekByMember( + date, + Long.valueOf(principal.getName()) + ) + ); + } + + @DeleteMapping("/{reservationId}") + public ApiResponse cancelReservation( + @PathVariable("reservationId") Long reservationId, + Principal principal + ) { + return ApiResponse.ok( + reservationService.cancel( + Long.valueOf(principal.getName()), + reservationId + ) + ); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ChangeReservationRequest.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ChangeReservationRequest.java new file mode 100644 index 0000000..bd6b275 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ChangeReservationRequest.java @@ -0,0 +1,11 @@ +package com.sideProject.PlanIT.domain.reservation.dto.reqeust; + +import lombok.Getter; + +import java.time.LocalDateTime; +import java.util.List; + +@Getter +public class ChangeReservationRequest { + List reservedTimes; +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ReservationRequest.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ReservationRequest.java new file mode 100644 index 0000000..e96308f --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/reqeust/ReservationRequest.java @@ -0,0 +1,8 @@ +package com.sideProject.PlanIT.domain.reservation.dto.reqeust; + +import lombok.Getter; + +@Getter +public class ReservationRequest { + Long programId; +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/response/ReservationResponse.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/response/ReservationResponse.java new file mode 100644 index 0000000..800b0fc --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/dto/response/ReservationResponse.java @@ -0,0 +1,68 @@ +package com.sideProject.PlanIT.domain.reservation.dto.response; + +import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; +import com.sideProject.PlanIT.domain.reservation.entity.Reservation; +import com.sideProject.PlanIT.domain.user.dto.member.response.EmployeeSemiResponseDto; +import com.sideProject.PlanIT.domain.user.dto.member.response.MemberSemiResponseDto; +import lombok.Builder; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Slf4j +@Getter +public class ReservationResponse { + Long id; + MemberSemiResponseDto member; + EmployeeSemiResponseDto employee; + Long programId; + String programName; + LocalDateTime reservationTime; + LocalTime programTime; + ReservationStatus status; + + @Builder + public ReservationResponse( + Long id, + MemberSemiResponseDto member, + EmployeeSemiResponseDto employee, + Long programId, + String programName, + LocalDateTime reservationTime, + LocalTime programTime, + ReservationStatus status + ) { + this.id = id; + this.member = member; + this.employee = employee; + this.programId = programId; + this.programName = programName; + this.reservationTime = reservationTime; + this.programTime = programTime; + this.status = status; + } + + public static ReservationResponse of(Reservation reservation) { + if(reservation.getStatus() == ReservationStatus.POSSIBLE) { + return ReservationResponse.builder() + .id(reservation.getId()) + .member(MemberSemiResponseDto.of(reservation.getMember())) + .employee(EmployeeSemiResponseDto.of(reservation.getEmployee())) + .status(reservation.getStatus()) + .build(); + } + + return ReservationResponse.builder() + .id(reservation.getId()) + .member(MemberSemiResponseDto.of(reservation.getMember())) + .employee(EmployeeSemiResponseDto.of(reservation.getEmployee())) + .programId(reservation.getProgram().getId()) + .programName(reservation.getProgram().getProduct().getName()) + .reservationTime(reservation.getReservedTime()) + .programTime(reservation.getClassTime()) + .status(reservation.getStatus()) + .build(); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/ENUM/ReservationStatus.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/ENUM/ReservationStatus.java new file mode 100644 index 0000000..803e5c5 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/ENUM/ReservationStatus.java @@ -0,0 +1,7 @@ +package com.sideProject.PlanIT.domain.reservation.entity.ENUM; + +public enum ReservationStatus { + RESERVED, + POSSIBLE, + FINISHED +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java new file mode 100644 index 0000000..2309458 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/entity/Reservation.java @@ -0,0 +1,71 @@ +package com.sideProject.PlanIT.domain.reservation.entity; + +import com.sideProject.PlanIT.domain.program.entity.Program; +import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; +import com.sideProject.PlanIT.domain.user.entity.Employee; +import com.sideProject.PlanIT.domain.user.entity.Member; +import jakarta.persistence.*; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.apache.catalina.User; + +import java.sql.Time; +import java.time.LocalDateTime; +import java.time.LocalTime; + +@Entity +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +public class Reservation { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "id") + private Long id; + + @Column + private LocalDateTime reservedTime; + + @Column + private LocalTime classTime; + + @Enumerated(EnumType.STRING) + private ReservationStatus status; + + @ManyToOne + @JoinColumn(name = "program_id") + private Program program; + + @ManyToOne + @JoinColumn(name = "employ_id") + private Employee employee; + + @ManyToOne + @JoinColumn(name = "member_id") + private Member member; + + @Builder + public Reservation( + LocalDateTime reservedTime, + LocalTime classTime, + ReservationStatus status, + Employee employee) { + this.reservedTime = reservedTime; + this.classTime = classTime; + this.status = status; + this.employee = employee; + } + + public void reservation(Program program, Member member) { + this.member = member; + this.program = program; + this.status = ReservationStatus.RESERVED; + } + + public void cancel() { + this.program = null; + this.member = null; + this.status = ReservationStatus.POSSIBLE; + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/repository/ReservationRepository.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/repository/ReservationRepository.java new file mode 100644 index 0000000..87eb68d --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/repository/ReservationRepository.java @@ -0,0 +1,30 @@ +package com.sideProject.PlanIT.domain.reservation.repository; + +import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; +import com.sideProject.PlanIT.domain.reservation.entity.Reservation; +import com.sideProject.PlanIT.domain.user.entity.Employee; +import com.sideProject.PlanIT.domain.user.entity.Member; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.List; + +@Repository +public interface ReservationRepository extends JpaRepository { + + @Query("SELECT r FROM Reservation r WHERE r.employee = :employee AND r.reservedTime IN :reservedTimes") + List findByEmployeeAndReservedTimeIn(@Param("employee") Employee employee, @Param("reservedTimes") List reservedTimes); + @Query("SELECT r FROM Reservation r WHERE r.employee = :employee AND r.reservedTime IN :reservedTimes AND r.status = :status") + List findByEmployeeAndReservedTimeInAndStatus(@Param("employee") Employee employee, + @Param("reservedTimes") List reservedTimes, + @Param("status") ReservationStatus status); + + @Query("SELECT r FROM Reservation r WHERE r.member = :member AND r.reservedTime BETWEEN :startDateTime AND :endDateTime") + List findByMemberAndDateTimeBetween(@Param("member") Member member, @Param("startDateTime") LocalDateTime startDateTime, @Param("endDateTime") LocalDateTime endDateTime); + + @Query("SELECT r FROM Reservation r WHERE r.employee = :employee AND r.reservedTime BETWEEN :startDateTime AND :endDateTime") + List findByEmployeeAndDateTimeBetween(@Param("employee") Employee employee, @Param("startDateTime") LocalDateTime startDateTime, @Param("endDateTime") LocalDateTime endDateTime); +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationService.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationService.java new file mode 100644 index 0000000..25a38e5 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationService.java @@ -0,0 +1,16 @@ +package com.sideProject.PlanIT.domain.reservation.service; + +import com.sideProject.PlanIT.domain.reservation.dto.response.ReservationResponse; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +public interface ReservationService { + String changeAvailability(List times, Long employeeId, Long userId); + String reservation(Long reservationId, Long userId, Long programId); + Map> findReservationForWeekByMember(LocalDate day, Long id); + List findReservationForWeekByEmployee(LocalDate day, Long id); + String cancel(Long userId, Long reservationId); +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java new file mode 100644 index 0000000..5382db6 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceImpl.java @@ -0,0 +1,189 @@ +package com.sideProject.PlanIT.domain.reservation.service; + +import com.sideProject.PlanIT.common.response.CustomException; +import com.sideProject.PlanIT.common.response.ErrorCode; +import com.sideProject.PlanIT.domain.product.entity.Product; +import com.sideProject.PlanIT.domain.product.entity.enums.ProductType; +import com.sideProject.PlanIT.domain.program.entity.Program; +import com.sideProject.PlanIT.domain.program.repository.ProgramRepository; +import com.sideProject.PlanIT.domain.reservation.dto.response.ReservationResponse; +import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; +import com.sideProject.PlanIT.domain.reservation.entity.Reservation; +import com.sideProject.PlanIT.domain.reservation.repository.ReservationRepository; +import com.sideProject.PlanIT.domain.user.entity.Employee; +import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; +import com.sideProject.PlanIT.domain.user.repository.EmployeeRepository; +import com.sideProject.PlanIT.domain.user.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.DayOfWeek; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.temporal.TemporalAdjusters; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ReservationServiceImpl implements ReservationService { + private final ReservationRepository reservationRepository; + private final MemberRepository memberRepository; + private final EmployeeRepository employeeRepository; + private final ProgramRepository programRepository; + + @Override + @Transactional + public String changeAvailability(List reservedTimes, Long employeeId, Long userId) { + Member member = memberRepository.findById(userId).orElseThrow(() -> + new CustomException("존재하지 않는 유저입니다.", ErrorCode.MEMBER_NOT_FOUND) + ); + Employee employee = employeeRepository.findById(employeeId).orElseThrow(() -> + new CustomException("존재하지 않는 직원입니다.", ErrorCode.EMPLOYEE_NOT_FOUND) + ); + + if(!Objects.equals(member.getId(), employee.getMember().getId())) { + throw new CustomException("권한이 없습니다.", ErrorCode.NO_AUTHORITY); + } + + List existingReservations + = reservationRepository.findByEmployeeAndReservedTimeIn(employee, reservedTimes); + + + // 기존 예약 삭제 + List reservedReservations = existingReservations.stream() + .filter(reservation -> reservation.getStatus() == ReservationStatus.POSSIBLE) + .collect(Collectors.toList()); + reservedReservations.forEach(reservationRepository::delete); + + // 새 예약 추가 (기존 예약이 없는 reservedTimes에 대해서만) + reservedTimes.forEach(reservedTime -> { + boolean exists = existingReservations.stream() + .anyMatch(reservation -> reservation.getReservedTime().equals(reservedTime)); + if (!exists) { + Reservation newReservation = Reservation.builder() + .reservedTime(reservedTime) + .status(ReservationStatus.POSSIBLE) + .employee(employee) + .classTime(LocalTime.of(1,0)) + .build(); + reservationRepository.save(newReservation); + } + }); + + return "ok"; + } + + //todo: 현재 시간에 따라 예약 가능 여부 체크 + @Override + @Transactional + public String reservation(Long reservationId, Long userId, Long programId) { + Member member = memberRepository.findById(userId).orElseThrow(() -> + new CustomException("존재하지 않는 유저입니다.", ErrorCode.MEMBER_NOT_FOUND) + ); + Program program = programRepository.findById(programId).orElseThrow(() -> + new CustomException("존재하지 않는 수업입니다.", ErrorCode.PROGRAM_NOT_FOUND) + ); + Reservation reservation = reservationRepository.findById(reservationId).orElseThrow(() -> + new CustomException(reservationId + "는 존재하지 않는 않는 예약입니다.", ErrorCode.RESERVATION_NOT_FOUND) + ); + + //todo : entity 역할로 넘기기 + if(reservation.getStatus() != ReservationStatus.POSSIBLE) { + throw new CustomException("예약 " + reservationId + "은 예약할 수 없습니다.", ErrorCode.NOT_YOUR_TRAINER); + } + + if(program.getProduct().getType() != ProductType.PT) { + throw new CustomException("program " + programId + " 은 PT권이 아닙니다.", ErrorCode.NOT_PT); + } + + if(!Objects.equals(program.getEmployee().getId(), reservation.getEmployee().getId())) { + throw new CustomException("유저 " + userId + "은 해당 트레이너에 예약할 수 없습니다.", ErrorCode.NOT_YOUR_TRAINER); + } + + //프로그램 상태 변경 + program.reservation(); + programRepository.save(program); + + //프로그램 예약 + reservation.reservation(program,member); + reservationRepository.save(reservation); + + return "ok"; + } + + + @Override + public Map> findReservationForWeekByMember(LocalDate date, Long userId) { + LocalDateTime startOfWeek = calStartOfWeek(date); + LocalDateTime endOfWeek = calEndOfWeek(date); + + Member member = memberRepository.findById(userId).orElseThrow(() -> + new CustomException("존재하지 않는 유저입니다.", ErrorCode.MEMBER_NOT_FOUND) + ); + + List reservations; + //트레이너이면 + if(member.getRole() == MemberRole.TRAINER) { + Employee employee = employeeRepository.findByMemberId(member.getId()).orElseThrow(() -> + new CustomException("존재하지 않는 트레이너입니다.", ErrorCode.MEMBER_NOT_FOUND) + ); + reservations = reservationRepository.findByEmployeeAndDateTimeBetween(employee,startOfWeek,endOfWeek); + } else { + reservations = reservationRepository.findByMemberAndDateTimeBetween(member,startOfWeek,endOfWeek); + } + + Map> reservationMap = reservations.stream() + .map(ReservationResponse::of) + .collect(Collectors.groupingBy(response -> response.getReservationTime().toLocalDate())); + return reservationMap; + } + + @Override + public List findReservationForWeekByEmployee(LocalDate date, Long employeeId) { + return List.of(ReservationResponse.builder().build(), ReservationResponse.builder().build()); + } + + //그 주의 월요일 00:00:00 + private LocalDateTime calStartOfWeek(LocalDate date) { + return date.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); + } + + //그 주의 일요일 23:59:59 + private LocalDateTime calEndOfWeek(LocalDate date) { + return date.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(LocalTime.MAX); + } + + + //todo: 취소 불가 시간 + @Override + @Transactional + public String cancel(Long userId, Long reservationId) { + Reservation reservation = reservationRepository.findById(reservationId).orElseThrow(() -> + new CustomException("예약 " + reservationId + "은 없는 예약입니다.",ErrorCode.RESERVATION_NOT_FOUND) + ); + + if(reservation.getStatus() != ReservationStatus.RESERVED || !Objects.equals(reservation.getMember().getId(), userId)) { + throw new CustomException("예약 " + reservationId + "은 취소할 수 없습니다.",ErrorCode.RESERVATION_NOT_FOUND); + } + + //프로그램 횟수 추가 + Program program = reservation.getProgram(); + program.cancelReservation(); + programRepository.save(program); + + //예약 취소 + reservation.cancel(); + reservationRepository.save(reservation); + + return "ok"; + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/ENUM/ScheduleAttendance.java b/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/ENUM/ScheduleAttendance.java deleted file mode 100644 index 41c23bb..0000000 --- a/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/ENUM/ScheduleAttendance.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.sideProject.PlanIT.domain.schedule.entity.ENUM; - -public enum ScheduleAttendance { - ATTNENDED, - LATE, - ABSENT -} diff --git a/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/Schedule.java b/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/Schedule.java deleted file mode 100644 index e5639cf..0000000 --- a/src/main/java/com/sideProject/PlanIT/domain/schedule/entity/Schedule.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.sideProject.PlanIT.domain.schedule.entity; - -import com.sideProject.PlanIT.domain.program.entity.Program; -import com.sideProject.PlanIT.domain.schedule.entity.ENUM.ScheduleAttendance; -import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -@Entity -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@Getter -public class Schedule { - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "id") - private Long id; - - @Column(name = "schedule_name") - private String name; - - @Column(name = "schedule_detail") - private String detail; - - @Column - private LocalDateTime startAt; - - @Column - private LocalDateTime endAt; - - @Enumerated(EnumType.STRING) - private ScheduleAttendance attendance; - - @ManyToOne - @JoinColumn(name = "program_id") - private Program program; - - @Builder - public Schedule(String name, String detail, LocalDateTime startAt, LocalDateTime endAt, ScheduleAttendance attendance, Program program) { - this.name = name; - this.detail = detail; - this.startAt = startAt; - this.endAt = endAt; - this.attendance = attendance; - this.program = program; - } -} diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/controller/AuthController.java b/src/main/java/com/sideProject/PlanIT/domain/user/controller/AuthController.java index cedec03..c806511 100644 --- a/src/main/java/com/sideProject/PlanIT/domain/user/controller/AuthController.java +++ b/src/main/java/com/sideProject/PlanIT/domain/user/controller/AuthController.java @@ -3,23 +3,21 @@ import com.sideProject.PlanIT.common.response.ApiResponse; import com.sideProject.PlanIT.domain.user.dto.member.response.JwtResponseDto; import com.sideProject.PlanIT.domain.user.service.AuthService; -import jakarta.servlet.http.HttpServletRequest; -import lombok.AllArgsConstructor; -import lombok.extern.slf4j.Slf4j; +import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestHeader; +import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController -@AllArgsConstructor -@Slf4j +@RequiredArgsConstructor +@RequestMapping("/member") public class AuthController { private final AuthService authService; @GetMapping("/refresh") public ApiResponse refreshAccessToken(@RequestHeader("Authorization") String authorizationHeader) { - log.info(authorizationHeader); return ApiResponse.ok(authService.refreshAccessToken(authorizationHeader)); } } diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/controller/SocialLoginController.java b/src/main/java/com/sideProject/PlanIT/domain/user/controller/SocialLoginController.java new file mode 100644 index 0000000..c8dd342 --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/user/controller/SocialLoginController.java @@ -0,0 +1,37 @@ +package com.sideProject.PlanIT.domain.user.controller; + +import com.sideProject.PlanIT.common.response.ApiResponse; +import com.sideProject.PlanIT.domain.user.dto.member.response.JwtResponseDto; +import com.sideProject.PlanIT.domain.user.service.SocialLoginService; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/login") +public class SocialLoginController { + + private final SocialLoginService socialLoginService; + + @GetMapping("/naver") + public ApiResponse naverLoginFormURI() { + return ApiResponse.ok(socialLoginService.getNaverLoginURI()); + } + + @GetMapping("/google") + public ApiResponse googleLoginFormURI() { + return ApiResponse.ok(socialLoginService.getGoogleLoginURI()); + } + @GetMapping("/oauth2/code/naver") + public ApiResponse loginToNaver(@RequestParam("code") String code) throws Exception { + return ApiResponse.ok(socialLoginService.naverSocialLogin(code)); + } + + @GetMapping("/oauth2/code/google") + public ApiResponse loginToGoogle(@RequestParam("code") String code) throws Exception { + return ApiResponse.ok(socialLoginService.googleSocialLogin(code)); + } +} diff --git a/src/main/java/com/sideProject/PlanIT/domain/user/service/SocialLoginService.java b/src/main/java/com/sideProject/PlanIT/domain/user/service/SocialLoginService.java new file mode 100644 index 0000000..ce9fb5d --- /dev/null +++ b/src/main/java/com/sideProject/PlanIT/domain/user/service/SocialLoginService.java @@ -0,0 +1,232 @@ +package com.sideProject.PlanIT.domain.user.service; + +import com.sideProject.PlanIT.common.util.JwtTokenProvider; +import com.sideProject.PlanIT.domain.user.dto.member.response.JwtResponseDto; +import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; +import com.sideProject.PlanIT.domain.user.repository.MemberRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import net.minidev.json.JSONObject; +import net.minidev.json.parser.JSONParser; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.*; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.time.LocalDate; + +@Service +@RequiredArgsConstructor +@Slf4j +public class SocialLoginService { + + // Kakao 정보 + @Value("${spring.naver.client-id}") + private String NAVER_CLIENT_ID; + @Value("${spring.naver.client-secret}") + private String NAVER_CLIENT_SECRET; + @Value("${spring.naver.login-uri}") + private String NAVER_LOGIN_URI; + @Value("${spring.naver.redirect-uri}") + private String NAVER_REDIRECT_URI; + + // Google 정보 + @Value("${spring.google.client-id}") + private String GOOGLE_CLIENT_ID; + @Value("${spring.google.client-secret}") + private String GOOGLE_CLIENT_SECRET; + @Value("${spring.google.login-uri}") + private String GOOGLE_LOGIN_URI; + @Value("${spring.google.redirect-uri}") + private String GOOGLE_REDIRECT_URI; + + private final static String NAVER_API_URI = "https://openapi.naver.com"; + private final static String NAVER_AUTH_URI = "https://nid.naver.com"; + + private final static String GOOGLE_AUTH_URI = "https://oauth2.googleapis.com"; + private final static String GOOGLE_API_URI = "https://www.googleapis.com"; + + private final MemberRepository memberRepository; + private final JwtTokenProvider jwtTokenProvider; + + public String getNaverLoginURI() { + return NAVER_LOGIN_URI + + "reponse_type=code" + + "&client_id=" + NAVER_CLIENT_ID + + "&redirect_uri=" + NAVER_REDIRECT_URI; + } + + public String getGoogleLoginURI() { + return GOOGLE_LOGIN_URI + + "client_id=" + GOOGLE_CLIENT_ID + + "&redirect_uri=" + GOOGLE_REDIRECT_URI + + "&response_type=code" + + "&scope=email profile"; + } + + public JwtResponseDto naverSocialLogin(String code) throws Exception { + Member loginMember = getNaverUserInfoWithToken(getTokenToNaver(code)); + return checkSocialLoginMember(loginMember); + } + + public JwtResponseDto googleSocialLogin(String code) throws Exception { + Member loginMember = getGoogleUserInfoWithToken(getTokenToGoogle(code)); + return checkSocialLoginMember(loginMember); + } + + private String getTokenToNaver(String code) throws Exception { + if (code == null) throw new Exception("Failed get authorization code"); + + String accessToken; + String refreshToken; + + try { + HttpHeaders headers = new HttpHeaders(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", NAVER_CLIENT_ID); + params.add("client_secret", NAVER_CLIENT_SECRET); + params.add("code", code); + params.add("redirect_uri", NAVER_REDIRECT_URI); + + RestTemplate restTemplate = new RestTemplate(); + HttpEntity> httpEntity = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange( + NAVER_AUTH_URI + "/oauth2.0/token", + HttpMethod.POST, + httpEntity, + String.class + ); + + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObject = (JSONObject) jsonParser.parse(response.getBody()); + + accessToken = (String) jsonObject.get("access_token"); + refreshToken = (String) jsonObject.get("refresh_token"); + } catch (Exception e) { + throw new Exception("API call failed"); + } + return accessToken; + } + + private String getTokenToGoogle(String code) throws Exception { + if (code == null) throw new Exception("Failed get authorization code"); + + String accessToken; + String refreshToken; + + try { + HttpHeaders headers = new HttpHeaders(); + + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", "authorization_code"); + params.add("client_id", GOOGLE_CLIENT_ID); + params.add("client_secret", GOOGLE_CLIENT_SECRET); + params.add("code", code); + params.add("redirect_uri", GOOGLE_REDIRECT_URI); + + RestTemplate restTemplate = new RestTemplate(); + HttpEntity> httpEntity = new HttpEntity<>(params, headers); + + ResponseEntity response = restTemplate.exchange( + GOOGLE_AUTH_URI + "/token", + HttpMethod.POST, + httpEntity, + String.class + ); + + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObject = (JSONObject) jsonParser.parse(response.getBody()); + + accessToken = (String) jsonObject.get("access_token"); + refreshToken = (String) jsonObject.get("refresh_token"); + } catch (Exception e) { + throw new Exception("API call failed"); + } + log.info(accessToken); + return accessToken; + } + + private Member getNaverUserInfoWithToken(String accessToken) throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + RestTemplate rt = new RestTemplate(); + HttpEntity> httpEntity = new HttpEntity<>(headers); + ResponseEntity response = rt.exchange( + NAVER_API_URI + "/v1/nid/me", + HttpMethod.POST, + httpEntity, + String.class + ); + + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody()); + JSONObject account = (JSONObject) jsonObj.get("response"); + + String id = String.valueOf(account.get("id")); + String email = String.valueOf(account.get("email")); + String name = String.valueOf(account.get("name")); + String phone_number = String.valueOf(account.get("mobile")); + String birth = String.join("-", String.valueOf(account.get("birthyear")), String.valueOf(account.get("birthday"))); + + return Member.builder() + .email(email) + .name(name) + .phone_number(phone_number) + .birth(LocalDate.parse(birth)) + .build(); + } + + private Member getGoogleUserInfoWithToken(String accessToken) throws Exception { + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + RestTemplate rt = new RestTemplate(); + HttpEntity> httpEntity = new HttpEntity<>(headers); + ResponseEntity response = rt.exchange( + GOOGLE_API_URI + "/oauth2/v1/userinfo?access_token=" + accessToken, + HttpMethod.GET, + httpEntity, + String.class + ); + + JSONParser jsonParser = new JSONParser(); + JSONObject jsonObj = (JSONObject) jsonParser.parse(response.getBody()); + + String email = String.valueOf(jsonObj.get("email")); + String name = String.valueOf(jsonObj.get("name")); + + log.info(email); + log.info(name); + return Member.builder() + .email(email) + .name(name) + .build(); + } + + private JwtResponseDto checkSocialLoginMember(Member member) { + Member socialLoginMember = memberRepository.findByEmail(member.getEmail()).orElse(Member.builder() + .email(member.getEmail()) + .name(member.getName()) + .phone_number(member.getPhone_number()) + .birth(member.getBirth()) + .role(MemberRole.MEMBER) + .build()); + + // 최초 소셜 로그인 + Member tokenMember = memberRepository.save(socialLoginMember); + + return JwtResponseDto.builder() + .accessToken(jwtTokenProvider.createAccessToken(tokenMember)) + .refreshToken(jwtTokenProvider.createRefreshToken(tokenMember)) + .build(); + } +} diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml deleted file mode 100644 index c722319..0000000 --- a/src/main/resources/application-dev.yml +++ /dev/null @@ -1,31 +0,0 @@ -# default -spring: - profiles: - active: - on-profile : dev # 기본 환경을 dev로 - - fileStorage: - dir: ${FILE_STORAGE_DIR} - - jwt: - secret-key: ${SECRET_KEY} - access-token-expire: 432000 - refresh-token-expire: 1209600000 - - datasource: - url: jdbc:mysql://${DB_CONNECTION_URL}test?serverTimezone=Asia/Seoul&characterEncoding=UTF-8 - username: ${USER_NAME} - password: ${PASSWORD} - driver-class-name: com.mysql.cj.jdbc.Driver - jpa: - hibernate.ddl-auto: create - properties: - hibernate: - format_sql: true - show_sql: true - - -# 환경변수 설정 -# - DB USER_NAME, PASSWORD -# - 파일 저장 디렉토리 경로 -# - jwt secret_key (추후 확정) diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index ac34e94..a1cbc8e 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -30,6 +30,19 @@ spring: cors: allowed-origins: "*" + + naver: + client-id: ${NAVER_CLIENT_ID} + client-secret: ${NAVER_CLIENT_SECRET} + login-uri: https://nid.naver.com/oauth2.0/authorize?response_type=code& + redirect-uri: http://localhost:8080/login/oauth2/code/naver + + google: + client-id: ${GOOGLE_CLIENT_ID} + client-secret: ${GOOGLE_CLIENT_SECRET} + login-uri: https://accounts.google.com/o/oauth2/v2/auth/oauthchooseaccount? + redirect-uri: http://localhost:8080/login/oauth2/code/google + # 환경변수 설정 # - DB USER_NAME, PASSWORD # - 파일 저장 디렉토리 경로 diff --git a/src/test/java/com/sideProject/PlanIT/domain/file/service/FileServiceTest/FileServiceTest.java b/src/test/java/com/sideProject/PlanIT/domain/file/service/FileServiceTest/FileServiceTest.java new file mode 100644 index 0000000..1c994de --- /dev/null +++ b/src/test/java/com/sideProject/PlanIT/domain/file/service/FileServiceTest/FileServiceTest.java @@ -0,0 +1,101 @@ +package com.sideProject.PlanIT.domain.file.service.FileServiceTest; + +import com.sideProject.PlanIT.domain.file.service.FileService; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.core.io.Resource; +import org.springframework.mock.web.MockMultipartFile; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.web.multipart.MultipartFile; + +import java.io.*; +import java.util.UUID; +import static org.assertj.core.api.Assertions.*; + +@Slf4j +@SpringBootTest +@ActiveProfiles("dev") +public class FileServiceTest { + + @Value("${spring.fileStorage.dir}") + private String fileStorageDir; + + @Autowired + FileService fileService; + + @Test + @DisplayName("파일을 저장합니다") + void saveFile() { + + // given + File testFile = new File(fileStorageDir + "test.txt"); + + try { + FileWriter writer = new FileWriter(testFile); + writer.write("Hello, World!"); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + MultipartFile multipartFile = null; + try (InputStream input = new FileInputStream(testFile)){ + multipartFile = new MockMultipartFile("file", testFile.getName(), "test/plain", input); + } catch (IOException e) { + e.printStackTrace(); + } + + // when + String fileName = fileService.saveFile(multipartFile); + + // then + File savedFile = new File(fileStorageDir + File.separator + fileName); + assertThat(savedFile.exists()).isTrue(); + } + + @Test + @DisplayName("저장된 파일을 가져옵니다") + void downloadFile() { + // given + File testFile = new File(fileStorageDir + "test.txt"); + + try { + FileWriter writer = new FileWriter(testFile); + writer.write("Hello, World!"); + writer.close(); + } catch (IOException e) { + e.printStackTrace(); + } + + MultipartFile multipartFile = null; + try (InputStream input = new FileInputStream(testFile)){ + multipartFile = new MockMultipartFile("file", testFile.getName(), "test/plain", input); + } catch (IOException e) { + e.printStackTrace(); + } + + String fileName = ""; + try { + UUID uuid = UUID.randomUUID(); + assert multipartFile != null; + fileName = uuid + multipartFile.getOriginalFilename(); + File dest = new File(fileStorageDir + File.separator + fileName); + multipartFile.transferTo(dest); + } catch (IOException e) { + e.printStackTrace(); + } + + // when + Resource resource = fileService.sendFile(fileName); + + // then + assertThat(resource.exists()).isTrue(); + assertThat(resource.isReadable()).isTrue(); + } + + +} diff --git a/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java b/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java new file mode 100644 index 0000000..3508b71 --- /dev/null +++ b/src/test/java/com/sideProject/PlanIT/domain/reservation/service/ReservationServiceTest.java @@ -0,0 +1,812 @@ +package com.sideProject.PlanIT.domain.reservation.service; + +import com.sideProject.PlanIT.common.response.CustomException; +import com.sideProject.PlanIT.domain.product.entity.Product; +import com.sideProject.PlanIT.domain.product.entity.enums.ProductType; +import com.sideProject.PlanIT.domain.product.repository.ProductRepository; +import com.sideProject.PlanIT.domain.program.entity.Program; +import com.sideProject.PlanIT.domain.program.entity.Registration; +import com.sideProject.PlanIT.domain.program.entity.enums.ProgramSearchStatus; +import com.sideProject.PlanIT.domain.program.entity.enums.RegistrationStatus; +import com.sideProject.PlanIT.domain.program.repository.ProgramRepository; +import com.sideProject.PlanIT.domain.program.repository.RegistrationRepository; +import com.sideProject.PlanIT.domain.reservation.dto.response.ReservationResponse; +import com.sideProject.PlanIT.domain.reservation.entity.ENUM.ReservationStatus; +import com.sideProject.PlanIT.domain.reservation.entity.Reservation; +import com.sideProject.PlanIT.domain.reservation.repository.ReservationRepository; +import com.sideProject.PlanIT.domain.user.entity.Employee; +import com.sideProject.PlanIT.domain.user.entity.Member; +import com.sideProject.PlanIT.domain.user.entity.enums.MemberRole; +import com.sideProject.PlanIT.domain.user.repository.EmployeeRepository; +import com.sideProject.PlanIT.domain.user.repository.MemberRepository; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.Period; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.Map; + +import static com.sideProject.PlanIT.domain.program.entity.enums.ProgramStatus.IN_PROGRESS; +import static org.assertj.core.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@SpringBootTest +@ActiveProfiles("dev") +class ReservationServiceTest { + + @Autowired + ProgramRepository programRepository; + @Autowired + ProductRepository productRepository; + @Autowired + EmployeeRepository employeeRepository; + @Autowired + MemberRepository memberRepository; + @Autowired + RegistrationRepository registrationRepository; + @Autowired + ReservationRepository reservationRepository; + @Autowired + ReservationService reservationService; + + DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd H:mm"); + + @AfterEach + void tearDown() { + reservationRepository.deleteAllInBatch(); + programRepository.deleteAllInBatch(); + registrationRepository.deleteAllInBatch(); + employeeRepository.deleteAllInBatch(); + memberRepository.deleteAllInBatch(); + productRepository.deleteAllInBatch(); + } + + private Product initProduct(String name, Period period, int number, ProductType type) { + Product product = Product.builder() + .name(name) + .period(period) + .number(number) + .price(30000) + .type(type) + .build(); + return productRepository.save(product); + } + + private Member initMember(String name, MemberRole role) { + Member member = Member.builder() + .name(name) + .email(name + "test.com") + .password("test123") + .birth(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .phone_number("010-0000-0000") + .role(role) + .build(); + + return memberRepository.save(member); + } + + private Employee initTrainer(String name) { + Member member = initMember(name, MemberRole.TRAINER); + + Employee employee = Employee.builder() + .member(memberRepository.save(member)) + .build(); + + return employeeRepository.save(employee); + } + + //todo : 실패 케이스 처리해야함 + @Nested + @DisplayName("addReservationTest") + class AddReservationTest { + @DisplayName("생성된 예약시간이 없으면 예약시간이 생성된다.") + @Test + void addReservation() { + //given + Employee trainer = initTrainer("trainer"); + + LocalDateTime time1 = LocalDateTime.of(2021, 1, 1, 10, 0, 0); + LocalDateTime time2 = LocalDateTime.of(2021, 1, 1, 11, 0, 0); + LocalDateTime time3 = LocalDateTime.of(2021, 1, 1, 12, 0, 0); + LocalDateTime time4 = LocalDateTime.of(2021, 1, 1, 13, 0, 0); + + List times = List.of(time1, time2, time3, time4); + //when + String result = reservationService.changeAvailability(times, trainer.getId(),trainer.getMember().getId()); + List registrations = reservationRepository.findAll(); + //then + assertThat(result).isEqualTo("ok"); + assertThat(registrations).hasSize(4); + assertThat(registrations).extracting( + "reservedTime", "status" + ).contains( + tuple(LocalDateTime.of(2021, 1, 1, 10, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 11, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 12, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 13, 0, 0), ReservationStatus.POSSIBLE) + ); + + assertThat(registrations) + .extracting((reservation) -> reservation.getEmployee().getId()) + .containsExactly(trainer.getId(), trainer.getId(), trainer.getId(), trainer.getId()); + } + + @DisplayName("생성된 예약시간이 있으면 삭제하고 없으면 예약시간이 생성된다.") + @Test + void addReservation2() { + //given + Employee trainer = initTrainer("trainer"); + + LocalDateTime time1 = LocalDateTime.of(2021, 1, 1, 10, 0, 0); + LocalDateTime time2 = LocalDateTime.of(2021, 1, 1, 11, 0, 0); + LocalDateTime time3 = LocalDateTime.of(2021, 1, 1, 12, 0, 0); + LocalDateTime time4 = LocalDateTime.of(2021, 1, 1, 13, 0, 0); + + Reservation saveReservation = Reservation.builder() + .reservedTime(time1) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .build(); + reservationRepository.save(saveReservation); + + List times = List.of(time1, time2, time3, time4); + //when + String result = reservationService.changeAvailability(times, trainer.getId(),trainer.getMember().getId()); + List registrations = reservationRepository.findAll(); + //then + assertThat(result).isEqualTo("ok"); + assertThat(registrations).hasSize(3); + assertThat(registrations).extracting( + "reservedTime", "status" + ).contains( + tuple(LocalDateTime.of(2021, 1, 1, 11, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 12, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 13, 0, 0), ReservationStatus.POSSIBLE) + ); + + assertThat(registrations) + .extracting((reservation) -> reservation.getEmployee().getId()) + .containsExactly(trainer.getId(), trainer.getId(), trainer.getId()); + } + + @DisplayName("예약된 예약은 삭제되지 않는다") + @Test + void addReservation3() { + //given + Employee trainer = initTrainer("trainer"); + + LocalDateTime time1 = LocalDateTime.of(2021, 1, 1, 10, 0, 0); + LocalDateTime time2 = LocalDateTime.of(2021, 1, 1, 11, 0, 0); + LocalDateTime time3 = LocalDateTime.of(2021, 1, 1, 12, 0, 0); + LocalDateTime time4 = LocalDateTime.of(2021, 1, 1, 13, 0, 0); + + Reservation saveReservation = Reservation.builder() + .reservedTime(time1) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .build(); + reservationRepository.save(saveReservation); + + List times = List.of(time1, time2, time3, time4); + //when + String result = reservationService.changeAvailability(times, trainer.getId(),trainer.getMember().getId()); + List registrations = reservationRepository.findAll(); + //then + assertThat(result).isEqualTo("ok"); + assertThat(registrations).hasSize(4); + assertThat(registrations).extracting( + "reservedTime", "status" + ).contains( + tuple(LocalDateTime.of(2021, 1, 1, 10, 0, 0), ReservationStatus.RESERVED), + tuple(LocalDateTime.of(2021, 1, 1, 11, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 12, 0, 0), ReservationStatus.POSSIBLE), + tuple(LocalDateTime.of(2021, 1, 1, 13, 0, 0), ReservationStatus.POSSIBLE) + ); + + assertThat(registrations) + .extracting((reservation) -> reservation.getEmployee().getId()) + .containsExactly(trainer.getId(), trainer.getId(), trainer.getId(), trainer.getId()); + } + } + + @Nested + @DisplayName("ReservationTest") + class ReservationTest { + + @DisplayName("PT권을 등록한 회원은 지정된 트레이너에 예약 가능하다.") + @Test + void reservationTest(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 10회", periodOfTenDays,10,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .employee(trainer) + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .remainedNumber(product.getNumber()) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .endAt(LocalDate.parse("2000-02-01", DateTimeFormatter.ISO_DATE)) + .build(); + programRepository.save(program); + + LocalDateTime reservationTime = LocalDateTime.of(2021, 1, 10, 10, 0, 0); + Reservation reservation = Reservation.builder() + .reservedTime(reservationTime) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + reservationRepository.save(reservation); + //when + String result = reservationService.reservation(reservation.getId(), member1.getId(), program.getId()); + List reservation1 = reservationRepository.findAll(); + + //then + assertThat(result).isEqualTo("ok"); + assertThat(reservation1).hasSize(1); + assertThat(reservation1).extracting( + "reservedTime", "status" + ).contains( + tuple(reservationTime, ReservationStatus.RESERVED) + ); + assertThat(reservation1.get(0).getMember().getId()).isEqualTo(member1.getId()); + assertThat(reservation1.get(0).getEmployee().getId()).isEqualTo(trainer.getId()); + assertThat(reservation1.get(0).getProgram().getId()).isEqualTo(program.getId()); + } + + @DisplayName("PT권으로 예약시 남은 횟수가 차감된다.") + @Test + void reservationTestMinusRemainNum(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 10회", periodOfTenDays,10,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .employee(trainer) + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .remainedNumber(product.getNumber()) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .endAt(LocalDate.parse("2000-02-01", DateTimeFormatter.ISO_DATE)) + .build(); + programRepository.save(program); + + LocalDateTime reservationTime = LocalDateTime.of(2021, 1, 10, 10, 0, 0); + Reservation reservation = Reservation.builder() + .reservedTime(reservationTime) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + reservationRepository.save(reservation); + //when + String result = reservationService.reservation(reservation.getId(), member1.getId(), program.getId()); + List reservation1 = reservationRepository.findAll(); + + //then + assertThat(result).isEqualTo("ok"); + assertThat(reservation1).hasSize(1); + assertThat(reservation1).extracting( + "reservedTime", "status" + ).contains( + tuple(reservationTime, ReservationStatus.RESERVED) + ); + assertThat(reservation1.get(0).getMember().getId()).isEqualTo(member1.getId()); + assertThat(reservation1.get(0).getEmployee().getId()).isEqualTo(trainer.getId()); + assertThat(reservation1.get(0).getProgram().getId()).isEqualTo(program.getId()); + assertThat(reservation1.get(0).getProgram().getRemainedNumber()).isEqualTo(program.getRemainedNumber()-1); + } + + @DisplayName("PT권을 등록한 회원은 지정된 트레이너가 아닌 사람에게 예약시 예외가 발생한다") + @Test + void reservationTest2(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 10회", periodOfTenDays,10,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Employee trainer2 = initTrainer("trainer2"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .employee(trainer2) + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .endAt(LocalDate.parse("2000-02-01", DateTimeFormatter.ISO_DATE)) + .build(); + programRepository.save(program); + + LocalDateTime reservationTime = LocalDateTime.of(2021, 1, 10, 10, 0, 0); + Reservation reservation = Reservation.builder() + .reservedTime(reservationTime) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + reservationRepository.save(reservation); + //when //then + assertThatThrownBy(() -> reservationService.reservation(reservation.getId(), member1.getId(), program.getId())) + .isInstanceOf(CustomException.class) + .hasMessage("유저 " + member1.getId()+ "은 해당 트레이너에 예약할 수 없습니다."); + } + + @DisplayName("PT권을 등록하지 않은 회원은 예약시 예외가 발생한다") + @Test + void reservationTest3(){ + //given + Period periodOfTenDays = Period.ofMonths(1); + Product product = initProduct("30일 회원권", periodOfTenDays,0,ProductType.MEMBERSHIP); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .endAt(LocalDate.parse("2000-02-01", DateTimeFormatter.ISO_DATE)) + .build(); + programRepository.save(program); + + LocalDateTime reservationTime = LocalDateTime.of(2021, 1, 10, 10, 0, 0); + Reservation reservation = Reservation.builder() + .reservedTime(reservationTime) + .employee(trainer) + .status(ReservationStatus.POSSIBLE) + .classTime(LocalTime.of(1,0)) + .build(); + reservationRepository.save(reservation); + //when //then + assertThatThrownBy(() -> reservationService.reservation(reservation.getId(), member1.getId(), program.getId())) + .isInstanceOf(CustomException.class) + .hasMessage("program " + program.getId()+ " 은 PT권이 아닙니다."); + } + + @DisplayName("이미 예약된 예약은 예약시 예외가 발생한다") + @Test + void reservationTest4(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-01-01 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2000-01-01", DateTimeFormatter.ISO_DATE)) + .endAt(LocalDate.parse("2000-02-01", DateTimeFormatter.ISO_DATE)) + .build(); + programRepository.save(program); + + LocalDateTime reservationTime = LocalDateTime.of(2021, 1, 10, 10, 0, 0); + Reservation reservation = Reservation.builder() + .reservedTime(reservationTime) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + reservation.reservation(program,member1); + Reservation reservation1 = reservationRepository.save(reservation); + + //when //then + assertThatThrownBy(() -> reservationService.reservation(reservation.getId(), member1.getId(), program.getId())) + .isInstanceOf(CustomException.class) + .hasMessage("예약 " + reservation1.getId() + "은 예약할 수 없습니다."); + } + } + + //todo : 에러 처리 테스트 구현 + @Nested + @DisplayName("FindReservationByMemberTest") + class FindReservationTest { + @DisplayName("회원의 이번주 예약을 확인할 수 있다.") + @Test + void findReservationByMemberTest(){ + //given + + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2024-03-10 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-03-10 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2024-03-10", DateTimeFormatter.ISO_DATE)) + .build(); + Program program1 = programRepository.save(program); + + LocalDateTime reservationTime1 = LocalDateTime.of(2024, 3, 18, 10, 0, 0); + Reservation reservation1 = Reservation.builder() + .reservedTime(reservationTime1) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime2 = LocalDateTime.of(2024, 3, 19, 10, 0, 0); + Reservation reservation2 = Reservation.builder() + .reservedTime(reservationTime2) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime3 = LocalDateTime.of(2024, 3, 17, 10, 0, 0); + Reservation reservation3 = Reservation.builder() + .reservedTime(reservationTime3) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + reservation1.reservation(program1,member1); + reservation2.reservation(program1,member1); + reservation3.reservation(program1,member1); + + LocalDate today = LocalDate.of(2024, 3, 19); + LocalDate today2 = LocalDate.of(2024, 3, 15); + + List reservations = List.of(reservation1,reservation2,reservation3); + reservationRepository.saveAll(reservations); + //when + Map>result1 = reservationService.findReservationForWeekByMember(today, member1.getId()); + Map> result2 = reservationService.findReservationForWeekByMember(today2, member1.getId()); + + + //then + assertThat(result1).hasSize(2); + assertThat(result2).hasSize(1); + assertThat(result1.get(LocalDate.of(2024, 3, 18)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime1, ReservationStatus.RESERVED + ); + + assertThat(result1.get(LocalDate.of(2024, 3, 19)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime2, ReservationStatus.RESERVED + ); + + assertThat(result2.get(LocalDate.of(2024, 3, 17)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime3, ReservationStatus.RESERVED + ); + } + + @DisplayName("트레이너의 이번주 예약을 확인할 수 있다.") + @Test + void findReservationByEmployeeTest(){ + //given + + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2024-03-10 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-03-10 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2024-03-10", DateTimeFormatter.ISO_DATE)) + .build(); + Program program1 = programRepository.save(program); + + LocalDateTime reservationTime1 = LocalDateTime.of(2024, 3, 18, 10, 0, 0); + Reservation reservation1 = Reservation.builder() + .reservedTime(reservationTime1) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime2 = LocalDateTime.of(2024, 3, 19, 10, 0, 0); + Reservation reservation2 = Reservation.builder() + .reservedTime(reservationTime2) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime3 = LocalDateTime.of(2024, 3, 17, 10, 0, 0); + Reservation reservation3 = Reservation.builder() + .reservedTime(reservationTime3) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + reservation1.reservation(program1,member1); + reservation2.reservation(program1,member1); + reservation3.reservation(program1,member1); + + LocalDate today = LocalDate.of(2024, 3, 19); + LocalDate today2 = LocalDate.of(2024, 3, 15); + + List reservations = List.of(reservation1,reservation2,reservation3); + List reservationList = reservationRepository.saveAll(reservations); + //when + Map> result1 = reservationService.findReservationForWeekByMember(today, trainer.getMember().getId()); + Map> result2 = reservationService.findReservationForWeekByMember(today2, trainer.getMember().getId()); + + //then + assertThat(result1).hasSize(2); + assertThat(result2).hasSize(1); + assertThat(result1.get(LocalDate.of(2024, 3, 18)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime1, ReservationStatus.RESERVED + ); + + assertThat(result1.get(LocalDate.of(2024, 3, 19)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime2, ReservationStatus.RESERVED + ); + + assertThat(result2.get(LocalDate.of(2024, 3, 17)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime3, ReservationStatus.RESERVED + ); + } + + @DisplayName("트레이너의 이번주 예약을 확인할 수 있다.") + @Test + void findEmptyReservation(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2024-03-10 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-03-10 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2024-03-10", DateTimeFormatter.ISO_DATE)) + .build(); + Program program1 = programRepository.save(program); + + LocalDateTime reservationTime1 = LocalDateTime.of(2024, 3, 18, 10, 0, 0); + Reservation reservation1 = Reservation.builder() + .reservedTime(reservationTime1) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + LocalDateTime reservationTime2 = LocalDateTime.of(2024, 3, 19, 10, 0, 0); + Reservation reservation2 = Reservation.builder() + .reservedTime(reservationTime2) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + reservation1.reservation(program1,member1); + reservation2.reservation(program1,member1); + + LocalDate today = LocalDate.of(2024, 3, 19); + LocalDate today2 = LocalDate.of(2024, 3, 15); + + List reservations = List.of(reservation1,reservation2); + List reservationList = reservationRepository.saveAll(reservations); + //when + Map> result1 = reservationService.findReservationForWeekByMember(today, trainer.getMember().getId()); + Map> result2 = reservationService.findReservationForWeekByMember(today2, trainer.getMember().getId()); + + + //then + assertThat(result1).hasSize(2); + assertThat(result2).hasSize(0); + assertThat(result1.get(LocalDate.of(2024, 3, 18)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime1, ReservationStatus.RESERVED + ); + + assertThat(result1.get(LocalDate.of(2024, 3, 19)).get(0)).extracting( + "programId","reservationTime", "status" + ).containsExactly( + program1.getId(),reservationTime2, ReservationStatus.RESERVED + ); + + } + } + + @Nested + @DisplayName("CancelTest") + class CancelTest { + @DisplayName("") + @Test + void test(){ + //given + Period periodOfTenDays = Period.ofMonths(0); + Product product = initProduct("PT 30회권", periodOfTenDays,30,ProductType.PT); + Employee trainer = initTrainer("trainer"); + Member member1 = initMember("tester1",MemberRole.MEMBER); + + Registration registration = Registration.builder() + .product(product) + .member(member1) + .discount(0) + .totalPrice(30000) + .status(RegistrationStatus.ACCEPTED) + .paymentAt(LocalDateTime.parse("2024-03-10 00:00", DATE_TIME_FORMATTER)) + .registrationAt(LocalDateTime.parse("2000-03-10 00:00", DATE_TIME_FORMATTER)) + .refundAt(null) + .build(); + Registration saveRegistration = registrationRepository.save(registration); + + Program program = Program.builder() + .registration(saveRegistration) + .product(saveRegistration.getProduct()) + .member(saveRegistration.getMember()) + .employee(trainer) + .remainedNumber(product.getNumber()-1) + .status(IN_PROGRESS) + .startAt(LocalDate.parse("2024-03-10", DateTimeFormatter.ISO_DATE)) + .build(); + Program program1 = programRepository.save(program); + + LocalDateTime reservationTime1 = LocalDateTime.of(2024, 3, 18, 10, 0, 0); + Reservation reservation1 = Reservation.builder() + .reservedTime(reservationTime1) + .employee(trainer) + .status(ReservationStatus.RESERVED) + .classTime(LocalTime.of(1,0)) + .build(); + + reservation1.reservation(program1,member1); + + Reservation reservation = reservationRepository.save(reservation1); + //when + String result = reservationService.cancel(reservation.getMember().getId(),reservation.getId()); + Reservation resultReservation = reservationRepository.findById(reservation.getId()).orElseThrow(); + Program resultProgram = programRepository.findById(reservation.getProgram().getId()).orElseThrow(); + + //then + assertThat(result).isEqualTo("ok"); + assertThat(resultReservation).extracting( + "reservedTime", "status" + ).containsExactly( + reservationTime1, ReservationStatus.POSSIBLE + ); + assertThat(resultProgram.getRemainedNumber()).isEqualTo(product.getNumber()); + + } + } +} \ No newline at end of file diff --git a/src/test/java/com/sideProject/PlanIT/domain/user/service/MemberServiceTest.java b/src/test/java/com/sideProject/PlanIT/domain/user/service/MemberServiceTest.java new file mode 100644 index 0000000..c47389f --- /dev/null +++ b/src/test/java/com/sideProject/PlanIT/domain/user/service/MemberServiceTest.java @@ -0,0 +1,11 @@ +package com.sideProject.PlanIT.domain.user.service; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@Slf4j +@SpringBootTest +@ActiveProfiles("dev") +public class MemberServiceTest { +}