diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 00000000..2ac83713 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: 'sports_echo_ci' +on: + push: + branches: [ "feature/*", "hotfix" ] + pull_request: + branches: [ "dev1" ] +permissions: + contents: read + pull-requests: read +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Git Checkout + uses: actions/checkout@v4 + + # .properties file 생성 + - name: application.properties 생성 + run: | + touch ./src/main/resources/application-prod.yml + touch ./src/main/resources/application-test.yml + + echo "${{ secrets.PROD_YML }}" > ./application-prod.yml + echo "${{ secrets.TEST_PROD_YML }}" > ./application-test.yml + + - name: Java Setup + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'oracle' + cache: gradle + + - name: Build with Gradle + env: + SPRING_PROFILES_ACTIVE: test + run: | + ./gradlew clean build + + - name: Publish Unit Test Results + uses: EnricoMi/publish-unit-test-result-action@v1 + if: ${{ always() }} + with: + files: build/test-results/**/*.xml + + - name: Upload Jacoco Report + if: ${{ failure() }} + uses: actions/upload-artifact@v3 + with: + name: jacoco-report + path: build/reports/jacoco/test/html \ No newline at end of file diff --git a/src/main/java/com/sportsecho/common/oauth/OAuthUtil.java b/src/main/java/com/sportsecho/common/oauth/OAuthUtil.java new file mode 100644 index 00000000..68ee6b0a --- /dev/null +++ b/src/main/java/com/sportsecho/common/oauth/OAuthUtil.java @@ -0,0 +1,158 @@ +package com.sportsecho.common.oauth; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.sportsecho.common.oauth.exception.OAuthErrorCode; +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import java.net.URI; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpHeaders; +import org.springframework.http.RequestEntity; +import org.springframework.http.ResponseEntity; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.stereotype.Service; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +/** + * kakaoOAuthDocs: https://developers.kakao.com/docs/latest/ko/kakaologin/rest-api#request-token + * naverOAuthDocs: https://developers.naver.com/docs/login/devguide/devguide.md + * googleOAuthDocs: https://developers.google.com/identity/protocols/oauth2/web-server?hl=ko + * */ + +@Service +@Slf4j(topic = "OAUthUtil") +@RequiredArgsConstructor +public class OAuthUtil { + + @Value("${oauth.api.key.kakao}") + private String kakaoApiKey; + + @Value("${oauth.api.key.naver}") + private String naverApiKey; + + @Value("${oauth.api.secret.naver}") + private String naverApiSecret; + + @Value("${oauth.api.key.google}") + private String googleApiKey; + + @Value("${oauth.api.secret.google}") + private String googleApiSecret; + + private final MemberRepository memberRepository; + + private final RestTemplate restTemplate; + private final PasswordEncoder passwordEncoder; + + public JsonNode getToken(URI uri, SocialType socialType, String code) { + try { + // HTTP Header 생성 + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + RequestEntity> requestEntity = RequestEntity + .post(uri) + .headers(headers) + .body(generateBody(socialType, code)); + + // HTTP 요청 보내기 + ResponseEntity response = restTemplate.exchange( + requestEntity, + String.class + ); + + // HTTP 응답 (JSON) -> 액세스 토큰 파싱 + return new ObjectMapper().readTree(response.getBody()); + } catch (JsonProcessingException e) { + throw new GlobalException(OAuthErrorCode.ILLEGAL_REQUEST); + } + } + + public JsonNode getMemberInfo(URI uri, String accessToken) { + try { + // HTTP Header 생성 + HttpHeaders headers = new HttpHeaders(); + headers.add("Authorization", "Bearer " + accessToken); + headers.add("Content-type", "application/x-www-form-urlencoded;charset=utf-8"); + + RequestEntity> requestEntity = RequestEntity + .post(uri) + .headers(headers) + .body(new LinkedMultiValueMap<>()); + + //HTTP 요청 보내기 + ResponseEntity response = restTemplate.exchange( + requestEntity, + String.class + ); + + return new ObjectMapper().readTree(response.getBody()); + } catch(JsonProcessingException e) { + throw new GlobalException(OAuthErrorCode.ILLEGAL_REQUEST); + } + } + + public Member registerSocialMemberIfNeeded(Long socialId, String memberName, String email, SocialType socialType) { + Member socialMember = memberRepository.findBySocialIdAndSocialType(socialId, socialType).orElse(null); + + if (socialMember == null) { + // 소셜 사용자 email과 동일한 email 가진 회원이 있는지 확인 + Member sameEmailMember = memberRepository.findByEmail(email).orElse(null); + + if (sameEmailMember != null) { + socialMember = sameEmailMember; + } else { + String encodedPassword = passwordEncoder.encode(UUID.randomUUID().toString()); + + socialMember = Member.builder() + .memberName(memberName) + .email(email) + .password(encodedPassword) + .role(MemberRole.CUSTOMER) + .build(); + } + + //socialId update 및 저장 + socialMember = socialMember.updateSocialIdAndType(socialId, socialType); + memberRepository.save(socialMember); + } + + return socialMember; + } + + private MultiValueMap generateBody(SocialType socialType, String code) { + MultiValueMap body = new LinkedMultiValueMap<>(); + + if(SocialType.KAKAO.equals(socialType)) { + body.add("grant_type", "authorization_code"); + body.add("client_id", kakaoApiKey); + body.add("redirect_uri", "http://localhost:8080/api/members/kakao/callback"); + body.add("code", code); + } + if(SocialType.NAVER.equals(socialType)) { + body.add("grant_type", "authorization_code"); + body.add("client_id", naverApiKey); + body.add("client_secret", naverApiSecret); + body.add("code", code); + body.add("state", "9kgsGTfH4j7IyAkg"); + } + if(SocialType.GOOGLE.equals(socialType)) { + body.add("grant_type", "authorization_code"); + body.add("client_id", googleApiKey); + body.add("client_secret", googleApiSecret); + body.add("code", code); + body.add("redirect_uri", "http://localhost:8080/api/members/google/callback"); + } + + return body; + } +} diff --git a/src/main/java/com/sportsecho/common/oauth/SocialType.java b/src/main/java/com/sportsecho/common/oauth/SocialType.java new file mode 100644 index 00000000..adffc9ed --- /dev/null +++ b/src/main/java/com/sportsecho/common/oauth/SocialType.java @@ -0,0 +1,5 @@ +package com.sportsecho.common.oauth; + +public enum SocialType { + KAKAO, NAVER, GOOGLE +} diff --git a/src/main/java/com/sportsecho/common/oauth/exception/OAuthErrorCode.java b/src/main/java/com/sportsecho/common/oauth/exception/OAuthErrorCode.java new file mode 100644 index 00000000..eef06b3d --- /dev/null +++ b/src/main/java/com/sportsecho/common/oauth/exception/OAuthErrorCode.java @@ -0,0 +1,17 @@ +package com.sportsecho.common.oauth.exception; + +import com.sportsecho.global.exception.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum OAuthErrorCode implements BaseErrorCode { + + ILLEGAL_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 OAuth 로그인 요청입니다."), + ; + + private final HttpStatus status; + private final String msg; +} diff --git a/src/main/java/com/sportsecho/global/util/s3/AWSConfig.java b/src/main/java/com/sportsecho/global/util/s3/AWSConfig.java new file mode 100644 index 00000000..a4d8e4e7 --- /dev/null +++ b/src/main/java/com/sportsecho/global/util/s3/AWSConfig.java @@ -0,0 +1,32 @@ +package com.sportsecho.global.util.s3; + +import com.amazonaws.auth.AWSStaticCredentialsProvider; +import com.amazonaws.auth.BasicAWSCredentials; +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.AmazonS3ClientBuilder; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AWSConfig { + + @Value("${cloud.aws.credentials.accessKey}") + private String iamAccessKey; // IAM Access Key + + @Value("${cloud.aws.credentials.secretKey}") + private String iamSecretKey; // IAM Secret Key + + private String region = "ap-northeast-2"; // Bucket Region (서울) + + @Bean + public AmazonS3Client amazonS3Client() { + BasicAWSCredentials basicAWSCredentials = new BasicAWSCredentials(iamAccessKey, iamSecretKey); + return (AmazonS3Client) AmazonS3ClientBuilder.standard() + .withRegion(region) + .withCredentials(new AWSStaticCredentialsProvider(basicAWSCredentials)) + .build(); + } + + +} diff --git a/src/main/java/com/sportsecho/global/util/s3/S3Uploader.java b/src/main/java/com/sportsecho/global/util/s3/S3Uploader.java new file mode 100644 index 00000000..d7c913e6 --- /dev/null +++ b/src/main/java/com/sportsecho/global/util/s3/S3Uploader.java @@ -0,0 +1,51 @@ +package com.sportsecho.global.util.s3; + +import com.amazonaws.services.s3.AmazonS3Client; +import com.amazonaws.services.s3.model.PutObjectRequest; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Slf4j +@RequiredArgsConstructor +@Service +public class S3Uploader { + + private final AmazonS3Client amazonS3Client; + + @Value("${cloud.aws.s3.bucket}") + private String bucket; + + @Value("${cloud.aws.region.static}") + private String region; + + public String upload(MultipartFile file, String filename) { + File fileObj = converMultiPartFileToFile(file); + amazonS3Client.putObject(new PutObjectRequest(bucket, filename, fileObj)); + fileObj.delete(); + + String fileurl = amazonS3Client.getUrl(bucket, filename).toString(); + + return fileurl; + } + + private File converMultiPartFileToFile(MultipartFile file) { + File convertedFile = new File(Objects.requireNonNull(file.getOriginalFilename())); + try (FileOutputStream fileOutputStream = new FileOutputStream(convertedFile)) { + fileOutputStream.write(file.getBytes()); + } catch (IOException e) { + log.error("파일 변환 실패 : ", e); + } + return convertedFile; + } + + public void deleteFile(String filename) { + amazonS3Client.deleteObject(bucket, filename); + } +} diff --git a/src/main/java/com/sportsecho/global/util/s3/controller/FileUploadController.java b/src/main/java/com/sportsecho/global/util/s3/controller/FileUploadController.java new file mode 100644 index 00000000..08c3442c --- /dev/null +++ b/src/main/java/com/sportsecho/global/util/s3/controller/FileUploadController.java @@ -0,0 +1,28 @@ +package com.sportsecho.global.util.s3.controller; + +import com.sportsecho.global.util.s3.service.FileUploadService; +import com.sportsecho.member.entity.MemberDetailsImpl; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api") +public class FileUploadController { + + private final FileUploadService fileUploadService; + + @PostMapping("/image") + public String uploadProductImage( + @RequestParam(value = "file")MultipartFile file, + @RequestParam(value = "identifier") String identifier, + @AuthenticationPrincipal MemberDetailsImpl memberDetails + ) { + return fileUploadService.uploadFile(memberDetails.getMember(), file, identifier); + } +} diff --git a/src/main/java/com/sportsecho/global/util/s3/service/FileUploadService.java b/src/main/java/com/sportsecho/global/util/s3/service/FileUploadService.java new file mode 100644 index 00000000..df2c075f --- /dev/null +++ b/src/main/java/com/sportsecho/global/util/s3/service/FileUploadService.java @@ -0,0 +1,31 @@ +package com.sportsecho.global.util.s3.service; + +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.global.util.s3.S3Uploader; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.product.exception.ProductErrorCode; +import com.sportsecho.product.repository.ProductRepository; +import java.util.UUID; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +@Service +@RequiredArgsConstructor +public class FileUploadService { + + private final S3Uploader s3Uploader; + + public String uploadFile(Member member, MultipartFile file, String identifier) { + +// if (member.getRole().equals(MemberRole.CUSTOMER)) { +// throw new GlobalException(ProductErrorCode.NO_AUTHORIZATION); +// } + + UUID uuid = UUID.randomUUID(); + String fileName = identifier + uuid; + + return s3Uploader.upload(file, fileName); + } +} diff --git a/src/main/java/com/sportsecho/hotdeal/scheduler/HotdealScheduler.java b/src/main/java/com/sportsecho/hotdeal/scheduler/HotdealScheduler.java new file mode 100644 index 00000000..f5c44d1e --- /dev/null +++ b/src/main/java/com/sportsecho/hotdeal/scheduler/HotdealScheduler.java @@ -0,0 +1,54 @@ +package com.sportsecho.hotdeal.scheduler; + +import com.sportsecho.hotdeal.entity.Hotdeal; +import com.sportsecho.hotdeal.repository.HotdealRepository; +import com.sportsecho.product.entity.Product; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Component +@RequiredArgsConstructor +public class HotdealScheduler { + + private final HotdealRepository hotdealRepository; + + // 매분마다 시행 + @Scheduled(cron = "0 * * * * *") + @Transactional + public void deleteClosedHotdeal() { + LocalDateTime now = LocalDateTime.now(); + List expiredHotdeals = hotdealRepository.findAllByDueDayBefore(now); + expiredHotdeals.stream() + .map(Hotdeal::getProduct) + .filter(Objects::nonNull) + .forEach(Product::unlinkHotdeal); // deleteAll 메소드 호출 전에 Product 엔티티와의 관계를 끊기 + + if (!expiredHotdeals.isEmpty()) { + log.info("마감시간이 지난 HOTDEAL {}개를 삭제하겠습니다.", expiredHotdeals.size()); + hotdealRepository.deleteAll(expiredHotdeals); + hotdealRepository.flush(); + List expiredHotdealsForCheck = hotdealRepository.findAllByDueDayBefore(now); + log.info(String.valueOf(expiredHotdealsForCheck.size())); + } + + List hotdealsWithZeroQuantity = hotdealRepository.findAllByDealQuantity(0); + hotdealsWithZeroQuantity.stream() + .map(Hotdeal::getProduct) + .filter(Objects::nonNull) + .forEach(Product::unlinkHotdeal); + + if (!hotdealsWithZeroQuantity.isEmpty()) { + log.info("한정수령이 모두 판매된 Hotdeal {}개를 삭제하겠습니다.", hotdealsWithZeroQuantity.size()); + hotdealRepository.deleteAll(hotdealsWithZeroQuantity); + hotdealRepository.flush(); + } + } + +} diff --git a/src/main/java/com/sportsecho/purchase/controller/PurchaseController.java b/src/main/java/com/sportsecho/purchase/controller/PurchaseController.java new file mode 100644 index 00000000..abf2ebff --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/controller/PurchaseController.java @@ -0,0 +1,48 @@ +package com.sportsecho.purchase.controller; + +import com.sportsecho.member.entity.MemberDetailsImpl; +import com.sportsecho.purchase.dto.PurchaseRequestDto; +import com.sportsecho.purchase.dto.PurchaseResponseDto; +import com.sportsecho.purchase.service.PurchaseService; +import java.util.List; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api/purchase") +public class PurchaseController { + + private final PurchaseService purchaseService; + + public PurchaseController(@Qualifier("V1") PurchaseService purchaseService) { + this.purchaseService = purchaseService; + } + + @PostMapping + public ResponseEntity purchase( + @RequestBody PurchaseRequestDto requestDto, + @AuthenticationPrincipal MemberDetailsImpl memberDetails + ) { + PurchaseResponseDto responseDto = purchaseService.purchase(requestDto, + memberDetails.getMember()); + + return ResponseEntity.status(HttpStatus.OK).body(responseDto); + } + + @GetMapping + public ResponseEntity> getPurchaseList( + @AuthenticationPrincipal MemberDetailsImpl memberDetails + ) { + List responseDtoList = purchaseService.getPurchaseList( + memberDetails.getMember()); + + return ResponseEntity.status(HttpStatus.OK).body(responseDtoList); + } +} diff --git a/src/main/java/com/sportsecho/purchase/dto/PurchaseListResponseDto.java b/src/main/java/com/sportsecho/purchase/dto/PurchaseListResponseDto.java new file mode 100644 index 00000000..8fef2c00 --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/dto/PurchaseListResponseDto.java @@ -0,0 +1,20 @@ +package com.sportsecho.purchase.dto; + +import com.sportsecho.purchaseProduct.dto.PurchaseProductResponseDto; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class PurchaseListResponseDto { + + private LocalDateTime createdAt; + + private List responseDtoList; +} diff --git a/src/main/java/com/sportsecho/purchase/dto/PurchaseRequestDto.java b/src/main/java/com/sportsecho/purchase/dto/PurchaseRequestDto.java new file mode 100644 index 00000000..e820ec67 --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/dto/PurchaseRequestDto.java @@ -0,0 +1,14 @@ +package com.sportsecho.purchase.dto; + +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +public class PurchaseRequestDto { + + private String address; + + private String phone; + +} diff --git a/src/main/java/com/sportsecho/purchase/dto/PurchaseResponseDto.java b/src/main/java/com/sportsecho/purchase/dto/PurchaseResponseDto.java new file mode 100644 index 00000000..491ac62a --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/dto/PurchaseResponseDto.java @@ -0,0 +1,26 @@ +package com.sportsecho.purchase.dto; + +import com.sportsecho.purchaseProduct.dto.PurchaseProductResponseDto; +import java.time.LocalDateTime; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class PurchaseResponseDto { + + private int totalPrice; + + private String address; + + private String phone; + + private LocalDateTime purchaseDate; + + private List responseDtoList; +} diff --git a/src/main/java/com/sportsecho/purchase/exception/PurchaseErrorCode.java b/src/main/java/com/sportsecho/purchase/exception/PurchaseErrorCode.java new file mode 100644 index 00000000..823fda1f --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/exception/PurchaseErrorCode.java @@ -0,0 +1,17 @@ +package com.sportsecho.purchase.exception; + +import com.sportsecho.global.exception.BaseErrorCode; +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +@AllArgsConstructor +public enum PurchaseErrorCode implements BaseErrorCode { + + EMPTY_CART(HttpStatus.BAD_REQUEST, "장바구니가 비어있습니다."), + EMPTY_PURCHASE_LIST(HttpStatus.BAD_REQUEST, "구매 내역이 없습니다."); + + private final HttpStatus status; + private final String msg; +} diff --git a/src/main/java/com/sportsecho/purchase/mapper/PurchaseMapper.java b/src/main/java/com/sportsecho/purchase/mapper/PurchaseMapper.java new file mode 100644 index 00000000..26897a87 --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/mapper/PurchaseMapper.java @@ -0,0 +1,21 @@ +package com.sportsecho.purchase.mapper; + +import com.sportsecho.member.entity.Member; +import com.sportsecho.purchase.dto.PurchaseRequestDto; +import com.sportsecho.purchase.entity.Purchase; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PurchaseMapper { + + PurchaseMapper INSTANCE = Mappers.getMapper(PurchaseMapper.class); + +// PurchaseResponseDto toResponseDto(Purchase purchase); + + + @Mapping(target = "totalPrice", constant = "0") + @Mapping(target = "member", source = "member") + Purchase toEntity(PurchaseRequestDto requestDto, Member member); +} diff --git a/src/main/java/com/sportsecho/purchase/repository/PurchaseRepository.java b/src/main/java/com/sportsecho/purchase/repository/PurchaseRepository.java new file mode 100644 index 00000000..93344508 --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/repository/PurchaseRepository.java @@ -0,0 +1,10 @@ +package com.sportsecho.purchase.repository; + +import com.sportsecho.purchase.entity.Purchase; +import java.util.List; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PurchaseRepository extends JpaRepository { + + List findByMemberId(Long memberId); +} diff --git a/src/main/java/com/sportsecho/purchase/service/PurchaseService.java b/src/main/java/com/sportsecho/purchase/service/PurchaseService.java new file mode 100644 index 00000000..bdb08b2d --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/service/PurchaseService.java @@ -0,0 +1,24 @@ +package com.sportsecho.purchase.service; + +import com.sportsecho.member.entity.Member; +import com.sportsecho.purchase.dto.PurchaseRequestDto; +import com.sportsecho.purchase.dto.PurchaseResponseDto; +import java.util.List; + +public interface PurchaseService { + + /*** + * 구매 API + * @param requestDto 배송지 정보 + * @param member 유저 정보 + * @return 배송지, 상품 정보 + */ + PurchaseResponseDto purchase(PurchaseRequestDto requestDto, Member member); + + /*** + * 구매 목록 조회 API + * @param member 유저 정보 + * @return 구매 상품 목록 + */ + List getPurchaseList(Member member); +} diff --git a/src/main/java/com/sportsecho/purchase/service/PurchaseServiceImplV1.java b/src/main/java/com/sportsecho/purchase/service/PurchaseServiceImplV1.java new file mode 100644 index 00000000..fbb2b511 --- /dev/null +++ b/src/main/java/com/sportsecho/purchase/service/PurchaseServiceImplV1.java @@ -0,0 +1,90 @@ +package com.sportsecho.purchase.service; + +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.entity.Member; +import com.sportsecho.memberProduct.entity.MemberProduct; +import com.sportsecho.memberProduct.repository.MemberProductRepository; +import com.sportsecho.purchase.dto.PurchaseRequestDto; +import com.sportsecho.purchase.dto.PurchaseResponseDto; +import com.sportsecho.purchase.entity.Purchase; +import com.sportsecho.purchase.exception.PurchaseErrorCode; +import com.sportsecho.purchase.mapper.PurchaseMapper; +import com.sportsecho.purchase.repository.PurchaseRepository; +import com.sportsecho.purchaseProduct.entity.PurchaseProduct; +import com.sportsecho.purchaseProduct.repository.PurchaseProductRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; + +@Qualifier("V1") +@Service +@RequiredArgsConstructor +public class PurchaseServiceImplV1 implements PurchaseService { + + private final PurchaseRepository purchaseRepository; + private final PurchaseProductRepository purchaseProductRepository; + private final MemberProductRepository memberProductRepository; + + @Override + @Transactional + public PurchaseResponseDto purchase(PurchaseRequestDto requestDto, Member member) { + + // 장바구니 목록 찾아오기 + Purchase purchase = PurchaseMapper.INSTANCE.toEntity(requestDto, member); + List memberProductList = memberProductRepository.findByMemberId( + member.getId()); + if (memberProductList.isEmpty()) { + throw new GlobalException(PurchaseErrorCode.EMPTY_CART); + } + purchaseRepository.save(purchase); + + // 구매 정보와 장바구니 상품 리스트로 purchaseProduct 엔티티 리스트 생성 + List purchaseProductList = createPList(memberProductList, purchase); + purchaseProductRepository.saveAll(purchaseProductList); + purchase.getPurchaseProductList().addAll(purchaseProductList); + + // 총 금액 업데이트 + purchase.updateTotalPrice(calTotalPrice(purchaseProductList)); + purchaseRepository.save(purchase); + + // 장바구니 비우기 + memberProductRepository.deleteAllByMemberId(member.getId()); + + return purchase.createResponseDto(); + } + + @Override + @Transactional(readOnly = true) + public List getPurchaseList(Member member) { + + List purchaseList = purchaseRepository.findByMemberId(member.getId()); + if (purchaseList.isEmpty()) { + throw new GlobalException(PurchaseErrorCode.EMPTY_PURCHASE_LIST); + } + + return purchaseList.stream() + .map(Purchase::createResponseDto) + .toList(); + } + + private int calTotalPrice(List purchaseProductList) { + return purchaseProductList.stream() + .mapToInt(purchaseProduct -> purchaseProduct.getProduct().getPrice() + * purchaseProduct.getProductsQuantity()) + .sum(); + } + + private List createPList(List memberProductList, + Purchase purchase) { + return memberProductList.stream() + .map(memberProduct -> PurchaseProduct.builder() + .purchase(purchase) + .product(memberProduct.getProduct()) + .productsQuantity(memberProduct.getProductsQuantity()) + .build()) + .toList(); + } +} diff --git a/src/main/java/com/sportsecho/purchaseProduct/dto/PurchaseProductResponseDto.java b/src/main/java/com/sportsecho/purchaseProduct/dto/PurchaseProductResponseDto.java new file mode 100644 index 00000000..aa2d0583 --- /dev/null +++ b/src/main/java/com/sportsecho/purchaseProduct/dto/PurchaseProductResponseDto.java @@ -0,0 +1,19 @@ +package com.sportsecho.purchaseProduct.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +public class PurchaseProductResponseDto { + + private int productsQuantity; + + private String title; + + private int price; +} diff --git a/src/main/java/com/sportsecho/purchaseProduct/mapper/PurchaseProductMapper.java b/src/main/java/com/sportsecho/purchaseProduct/mapper/PurchaseProductMapper.java new file mode 100644 index 00000000..52af5811 --- /dev/null +++ b/src/main/java/com/sportsecho/purchaseProduct/mapper/PurchaseProductMapper.java @@ -0,0 +1,17 @@ +package com.sportsecho.purchaseProduct.mapper; + +import com.sportsecho.purchaseProduct.dto.PurchaseProductResponseDto; +import com.sportsecho.purchaseProduct.entity.PurchaseProduct; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.mapstruct.factory.Mappers; + +@Mapper +public interface PurchaseProductMapper { + + PurchaseProductMapper INSTANCE = Mappers.getMapper(PurchaseProductMapper.class); + + @Mapping(target = "title", source = "product.title") + @Mapping(target = "price", source = "product.price") + PurchaseProductResponseDto toResponseDto(PurchaseProduct purchaseProduct); +} diff --git a/src/main/java/com/sportsecho/purchaseProduct/repository/PurchaseProductRepository.java b/src/main/java/com/sportsecho/purchaseProduct/repository/PurchaseProductRepository.java new file mode 100644 index 00000000..db8c3757 --- /dev/null +++ b/src/main/java/com/sportsecho/purchaseProduct/repository/PurchaseProductRepository.java @@ -0,0 +1,8 @@ +package com.sportsecho.purchaseProduct.repository; + +import com.sportsecho.purchaseProduct.entity.PurchaseProduct; +import org.springframework.data.jpa.repository.JpaRepository; + +public interface PurchaseProductRepository extends JpaRepository { + +} diff --git a/src/main/resources/application-test.yml b/src/main/resources/application-test.yml new file mode 100644 index 00000000..c23effa5 --- /dev/null +++ b/src/main/resources/application-test.yml @@ -0,0 +1,62 @@ +spring: + config: + activate: + on-profile: test + h2: + console: + enabled: true + jpa: + hibernate: + ddl-auto: update + datasource: + # driver-class-name: org.h2.Driver + # url: jdbc:h2:mem:testdb;MODE=MySQL + # username: SA + # password: '' + url: jdbc:mysql://sports-echo-mysql-instance.c8cvx8gfy4m3.ap-northeast-2.rds.amazonaws.com/echo_db_test + username: ${rds_username} + password: ${rds_password} + driver-class-name: com.mysql.cj.jdbc.Driver + servlet: + multipart: + enabled: true + max-file-size: 10MB + max-request-size: 10MB + data: + redis: + host: localhost + port: 6379 + +jwt: + secret_key: ${jwt.secret_key} + +api-keys: + football: ${football-api-key} + basketball: ${basketball-api-key} + baseball: ${baseball-api-key} + +cloud: + aws: + credentials: + accessKey: ${aws_accessKey} + secretKey: ${aws_secretKey} + region: + static: ap-northeast-2 + stack: + auto: false + s3: + bucket: sports-echo + +oauth: + api: + secret: + naver: ${naver-api-secret} + google: ${google-api-secret} + key: + kakao: ${kakao-api-key} + naver: ${naver-api-key} + google: ${google-api-key} + +admin: + key: + secret: ${admin-secret-key} diff --git a/src/main/resources/static/css/TestGame.css b/src/main/resources/static/css/TestGame.css new file mode 100644 index 00000000..62446655 --- /dev/null +++ b/src/main/resources/static/css/TestGame.css @@ -0,0 +1,74 @@ +body { + font-family: Arial, sans-serif; + margin: 0; + padding: 0; + background: #f4f4f4; +} +.container { + width: 80%; + margin: auto; + overflow: hidden; +} +header { + background: #50b3a2; + color: white; + padding-top: 30px; + min-height: 70px; + border-bottom: #eaeaea 1px solid; +} +header a { + color: #ffffff; + text-decoration: none; + text-transform: uppercase; + font-size: 16px; +} +header ul { + padding: 0; + margin: 0; + list-style: none; + overflow: hidden; +} +header li { + float: left; + display: inline; + padding: 0 20px 0 20px; +} +header #branding { + float: left; +} +header #branding h1 { + margin: 0; +} +header nav { + float: right; + margin-top: 10px; +} +header .highlight, header .current a { + color: #eaeaea; + font-weight: bold; +} +header a:hover { + color: #ffffff; + font-weight: bold; +} +.button { + height: 30px; + background: #333; + border: none; + padding-left: 20px; + padding-right: 20px; + color: #ffffff; +} +.game { + background: #ffffff; + padding: 20px; + margin-top: 20px; + border: 1px #eaeaea solid; +} +.game h3 { + margin-top: 0; + color: #333; +} +.game p { + margin-bottom: 10px; +} \ No newline at end of file diff --git a/src/main/resources/static/css/comment.css b/src/main/resources/static/css/comment.css new file mode 100644 index 00000000..e5993c84 --- /dev/null +++ b/src/main/resources/static/css/comment.css @@ -0,0 +1,138 @@ +body { + margin: 15px; + padding: 15px; + font-family: Arial, sans-serif; + /* 수정된 그라데이션 배경색 */ + background: linear-gradient(#76B6EA, #D4E6F1); + min-height: 100vh; /* 화면 전체 높이를 채우도록 설정 */ +} +/* 드롭다운 콘텐츠 */ +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown-content a:hover { background-color: #f1f1f1; } + +nav > ul > li:hover .dropdown-content { + display: block; +} +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + background-color: rgba(242, 242, 242, 0.8); + border-radius: 10px; +} + +nav { + padding: 20px 0; + border-radius: 10px; + background-color: rgba(162,213,242, 0.8); /* 네비게이션 바 배경색 및 투명도 설정 */ +} + + +nav ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + justify-content: center; +} + +nav ul li { + margin: 0 10px; +} + +nav ul li a { + text-decoration: none; + color: white; /* 네비게이션 바 글자 색상을 흰색으로 설정 */ + font-weight: bold; /* 글자를 볼드체로 설정 */ +} +#logo img { + width: 100px; /* 로고 이미지 크기 조정 */ + height: 100px; + border-radius: 50%; + object-fit: cover; +} +#logo, #user-profile { + display: flex; + align-items: center; +} + + +#user-profile img { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; + margin-left: 10px; +} +footer { + text-align: center; + padding: 2px 0; +} +h1 { + margin: 0; /* 중앙에 위치한 웹페이지 이름의 여백 제거 */ + font-weight: bold; + font-size: 60px; +} +/* 댓글 입력 폼 스타일링 */ +#comment-form { + margin-top: 20px; +} + +#comment-form textarea { + width: 100%; /* 입력 칸을 가로로 길게 설정 */ + height: 100px; /* 입력 칸의 높이 설정 */ + padding: 10px; + border: 1px solid #ccc; + border-radius: 5px; + font-size: 16px; + margin-bottom: 10px; /* 버튼과의 간격 */ +} + +#comment-form button { + width: 100px; /* 버튼의 너비 */ + padding: 10px 15px; + background-color: #A1D6E2; /* 버튼의 배경색 */ + color: white; + border: none; + border-radius: 5px; + cursor: pointer; +} + +#comment-form button:hover { + background-color: #52B2CF; /* 호버 시 버튼 색상 변경 */ +} + +/* 댓글 목록 스타일링 */ +#comments-container div { + margin-bottom: 10px; + padding: 10px; + background-color: #f2f2f2; + border-radius: 5px; +} +.comment-item { + background-color: #f0f0f0; + border-radius: 5px; + padding: 10px; + margin-bottom: 10px; +} + +.comment-item span { + font-size: 0.8em; + color: #777; +} \ No newline at end of file diff --git a/src/main/resources/static/css/index.css b/src/main/resources/static/css/index.css new file mode 100644 index 00000000..37a29bf5 --- /dev/null +++ b/src/main/resources/static/css/index.css @@ -0,0 +1,92 @@ +body { + margin: 15px; + padding: 15px; + font-family: Arial, sans-serif; + /* 수정된 그라데이션 배경색 */ + background: linear-gradient(#76B6EA, #D4E6F1); + min-height: 100vh; /* 화면 전체 높이를 채우도록 설정 */ +} +/* 드롭다운 콘텐츠 */ +.dropdown-content { + display: none; + position: absolute; + background-color: #f9f9f9; + min-width: 160px; + box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2); + z-index: 1; +} + +.dropdown-content a { + color: black; + padding: 12px 16px; + text-decoration: none; + display: block; +} + +.dropdown-content a:hover { background-color: #f1f1f1; } + +nav > ul > li:hover .dropdown-content { + display: block; +} +header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + background-color: rgba(242, 242, 242, 0.8); + border-radius: 10px; +} + +nav { + padding: 20px 0; + border-radius: 10px; + background-color: rgba(162,213,242, 0.8); /* 네비게이션 바 배경색 및 투명도 설정 */ +} + + +nav ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + justify-content: center; +} + +nav ul li { + margin: 0 10px; +} + +nav ul li a { + text-decoration: none; + color: white; /* 네비게이션 바 글자 색상을 흰색으로 설정 */ + font-weight: bold; /* 글자를 볼드체로 설정 */ +} +#logo img { + width: 100px; /* 로고 이미지 크기 조정 */ + height: 100px; + border-radius: 50%; + object-fit: cover; +} +#logo, #user-profile { + display: flex; + align-items: center; +} + + +#user-profile img { + width: 50px; + height: 50px; + border-radius: 50%; + object-fit: cover; + margin-left: 10px; +} +footer { + text-align: center; + padding: 2px 0; +} +h1 { + margin: 0; /* 중앙에 위치한 웹페이지 이름의 여백 제거 */ + font-weight: bold; + font-size: 60px; +} + diff --git a/src/main/resources/static/images/EchoLogo.jpeg b/src/main/resources/static/images/EchoLogo.jpeg new file mode 100644 index 00000000..2661212b Binary files /dev/null and b/src/main/resources/static/images/EchoLogo.jpeg differ diff --git a/src/main/resources/static/images/IMG_1851.JPG b/src/main/resources/static/images/IMG_1851.JPG new file mode 100644 index 00000000..1ace7658 Binary files /dev/null and b/src/main/resources/static/images/IMG_1851.JPG differ diff --git "a/src/main/resources/static/images/btnG_\354\225\204\354\235\264\354\275\230\354\233\220\355\230\225.png" "b/src/main/resources/static/images/btnG_\354\225\204\354\235\264\354\275\230\354\233\220\355\230\225.png" new file mode 100644 index 00000000..e3c3242f Binary files /dev/null and "b/src/main/resources/static/images/btnG_\354\225\204\354\235\264\354\275\230\354\233\220\355\230\225.png" differ diff --git a/src/main/resources/static/images/echologo2.jpeg b/src/main/resources/static/images/echologo2.jpeg new file mode 100644 index 00000000..385c364b Binary files /dev/null and b/src/main/resources/static/images/echologo2.jpeg differ diff --git a/src/main/resources/static/images/free-icon-github-sign-25657.png b/src/main/resources/static/images/free-icon-github-sign-25657.png new file mode 100644 index 00000000..969124f6 Binary files /dev/null and b/src/main/resources/static/images/free-icon-github-sign-25657.png differ diff --git a/src/main/resources/static/images/gitlogo.png b/src/main/resources/static/images/gitlogo.png new file mode 100644 index 00000000..c9a5b90a Binary files /dev/null and b/src/main/resources/static/images/gitlogo.png differ diff --git a/src/main/resources/static/images/google.png b/src/main/resources/static/images/google.png new file mode 100644 index 00000000..d938293b Binary files /dev/null and b/src/main/resources/static/images/google.png differ diff --git "a/src/main/resources/static/images/kakao\354\233\220\355\230\225.png" "b/src/main/resources/static/images/kakao\354\233\220\355\230\225.png" new file mode 100644 index 00000000..f4f87fe2 Binary files /dev/null and "b/src/main/resources/static/images/kakao\354\233\220\355\230\225.png" differ diff --git a/src/main/resources/static/images/shopping-cart.png b/src/main/resources/static/images/shopping-cart.png new file mode 100644 index 00000000..eda88a07 Binary files /dev/null and b/src/main/resources/static/images/shopping-cart.png differ diff --git a/src/main/resources/static/images/web_light_rd_na@2x.png b/src/main/resources/static/images/web_light_rd_na@2x.png new file mode 100644 index 00000000..4763cc14 Binary files /dev/null and b/src/main/resources/static/images/web_light_rd_na@2x.png differ diff --git "a/src/main/resources/static/images/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-04 \354\230\244\355\233\204 4.34.42.png" "b/src/main/resources/static/images/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-04 \354\230\244\355\233\204 4.34.42.png" new file mode 100644 index 00000000..210e6ec2 Binary files /dev/null and "b/src/main/resources/static/images/\354\212\244\355\201\254\353\246\260\354\203\267 2024-01-04 \354\230\244\355\233\204 4.34.42.png" differ diff --git a/src/main/resources/static/js/TestGame.js b/src/main/resources/static/js/TestGame.js new file mode 100644 index 00000000..76de58c2 --- /dev/null +++ b/src/main/resources/static/js/TestGame.js @@ -0,0 +1,30 @@ +function loadGames(sportType) { + $.ajax({ + url: '/api/games/' + sportType, + method: 'GET', + success: function(response) { + displayGames(response.data); + }, + error: function(error) { + console.error("Error loading games: ", error); + alert("경기 정보를 불러오는데 실패했습니다."); + } + }); +} + +function displayGames(games) { + var gamesContainer = $('#games'); + gamesContainer.empty(); + + games.forEach(function(game) { + var gameHtml = ` +
+

${game.teamA} vs ${game.teamB}

+

경기 시간: ${game.gameDateTime}

+

장소: ${game.location}

+ +
+ `; + gamesContainer.append(gameHtml); + }); +} \ No newline at end of file diff --git a/src/main/resources/static/js/baseball-comment.js b/src/main/resources/static/js/baseball-comment.js new file mode 100644 index 00000000..517de22b --- /dev/null +++ b/src/main/resources/static/js/baseball-comment.js @@ -0,0 +1,46 @@ +// API 키와 호스트 설정 +const apiKey = process.env.API_KEY; +const apiHostBaseball = 'api-baseball.p.rapidapi.com'; + +// 야구 경기 정보를 가져오는 함수 +function fetchBaseballGames() { + let apiUrl = 'https://api-baseball.p.rapidapi.com/timezone'; + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHostBaseball + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 야구 경기 일정을 페이지에 표시 + console.log(response); + }); +} + +// 야구 댓글을 불러오는 함수 +function fetchCommentsForBaseball(gameId) { + fetch(`/api/games/${gameId}/comments`) + .then(response => response.json()) + .then(comments => { + const commentsContainer = document.getElementById('baseball-comments-container'); + commentsContainer.innerHTML = ''; // 기존 내용 초기화 + + comments.forEach(comment => { + const commentElement = document.createElement('div'); + commentElement.className = 'comment-item'; + commentElement.innerHTML = ` +

${comment.memberName} (${formatDate(comment.createdDate)}): ${comment.content}

+ `; + commentsContainer.appendChild(commentElement); + }); + }) + .catch(error => console.error('Error:', error)); +} + +document.addEventListener('DOMContentLoaded', function() { + const gameId = '특정 야구 게임 ID'; // 실제 게임 ID로 대체 + fetchBaseballGames(); + fetchCommentsForBaseball(gameId); +}); diff --git a/src/main/resources/static/js/baseball.js b/src/main/resources/static/js/baseball.js new file mode 100644 index 00000000..615c5753 --- /dev/null +++ b/src/main/resources/static/js/baseball.js @@ -0,0 +1,54 @@ +// API 키와 호스트는 변수로 저장하여 재사용 +const apiKey = process.env.API_KEY; +const apiHostFootball = 'api-football-v1.p.rapidapi.com'; +const apiHostBasketball = 'api-basketball.p.rapidapi.com'; +const apiHostBaseball = 'api-baseball.p.rapidapi.com'; + +// 경기 정보를 가져오는 함수 +function fetchGames(sportType) { + let apiUrl, apiHost; + + switch (sportType) { + case 'football': + apiUrl = 'https://api-football-v1.p.rapidapi.com/v3/timezone'; + apiHost = apiHostFootball; + break; + case 'basketball': + apiUrl = 'https://api-basketball.p.rapidapi.com/timezone'; + apiHost = apiHostBasketball; + break; + case 'baseball': + apiUrl = 'https://api-baseball.p.rapidapi.com/timezone'; + apiHost = apiHostBaseball; + break; + default: + return; + } + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHost + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 경기 일정을 페이지에 표시 + console.log(response); + }); +} +fetchGames('baseball').then(response => { + const baseballScheduleContainer = document.getElementById('baseball-games-schedule'); + baseballScheduleContainer.innerHTML = ''; // 기존 내용 초기화 + + // 예시: API 응답이 경기의 배열을 포함한다고 가정 + response.games.forEach(game => { + const gameInfo = document.createElement('div'); + gameInfo.innerHTML = ` +

${game.teamA} vs ${game.teamB}

+

Date: ${game.date}

+

Location: ${game.location}

+ `; + baseballScheduleContainer.appendChild(gameInfo); + }); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/basketball-comment.js b/src/main/resources/static/js/basketball-comment.js new file mode 100644 index 00000000..b1291c7f --- /dev/null +++ b/src/main/resources/static/js/basketball-comment.js @@ -0,0 +1,46 @@ +// API 키와 호스트 설정 +const apiKey = process.env.API_KEY; +const apiHostBasketball = 'api-basketball.p.rapidapi.com'; + +// 농구 경기 정보를 가져오는 함수 +function fetchBasketballGames() { + let apiUrl = 'https://api-basketball.p.rapidapi.com/timezone'; + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHostBasketball + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 농구 경기 일정을 페이지에 표시 + console.log(response); + }); +} + +// 농구 댓글을 불러오는 함수 +function fetchCommentsForBasketball(gameId) { + fetch(`/api/games/${gameId}/comments`) + .then(response => response.json()) + .then(comments => { + const commentsContainer = document.getElementById('basketball-comments-container'); + commentsContainer.innerHTML = ''; // 기존 내용 초기화 + + comments.forEach(comment => { + const commentElement = document.createElement('div'); + commentElement.className = 'comment-item'; + commentElement.innerHTML = ` +

${comment.memberName} (${formatDate(comment.createdDate)}): ${comment.content}

+ `; + commentsContainer.appendChild(commentElement); + }); + }) + .catch(error => console.error('Error:', error)); +} + +document.addEventListener('DOMContentLoaded', function() { + const gameId = '특정 농구 게임 ID'; // 실제 게임 ID로 대체 + fetchBasketballGames(); + fetchCommentsForBasketball(gameId); +}); diff --git a/src/main/resources/static/js/basketball.js b/src/main/resources/static/js/basketball.js new file mode 100644 index 00000000..9599fdb9 --- /dev/null +++ b/src/main/resources/static/js/basketball.js @@ -0,0 +1,54 @@ +// API 키와 호스트는 변수로 저장하여 재사용 +const apiKey = process.env.API_KEY; +const apiHostFootball = 'api-football-v1.p.rapidapi.com'; +const apiHostBasketball = 'api-basketball.p.rapidapi.com'; +const apiHostBaseball = 'api-baseball.p.rapidapi.com'; + +// 경기 정보를 가져오는 함수 +function fetchGames(sportType) { + let apiUrl, apiHost; + + switch (sportType) { + case 'football': + apiUrl = 'https://api-football-v1.p.rapidapi.com/v3/timezone'; + apiHost = apiHostFootball; + break; + case 'basketball': + apiUrl = 'https://api-basketball.p.rapidapi.com/timezone'; + apiHost = apiHostBasketball; + break; + case 'baseball': + apiUrl = 'https://api-baseball.p.rapidapi.com/timezone'; + apiHost = apiHostBaseball; + break; + default: + return; + } + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHost + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 경기 일정을 페이지에 표시 + console.log(response); + }); +} +fetchGames('basketball').then(response => { + const scheduleContainer = document.getElementById('basketball-games-schedule'); + scheduleContainer.innerHTML = ''; // 기존 내용 초기화 + + // 예시: API 응답이 경기의 배열을 포함한다고 가정 + response.games.forEach(game => { + const gameInfo = document.createElement('div'); + gameInfo.innerHTML = ` +

${game.teamA} vs ${game.teamB}

+

Date: ${game.date}

+

Location: ${game.location}

+ `; + scheduleContainer.appendChild(gameInfo); + }); +}); diff --git a/src/main/resources/static/js/football-comment.js b/src/main/resources/static/js/football-comment.js new file mode 100644 index 00000000..733cfb75 --- /dev/null +++ b/src/main/resources/static/js/football-comment.js @@ -0,0 +1,53 @@ +// API 키와 호스트 설정 +const apiKey = process.env.API_KEY; +const apiHostFootball = 'api-football-v1.p.rapidapi.com'; + +// 축구 경기 정보를 가져오는 함수 +function fetchFootballGames() { + let apiUrl = 'https://api-football-v1.p.rapidapi.com/v3/timezone'; + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHostFootball + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 축구 경기 일정을 페이지에 표시 + console.log(response); + }); +} + +// 축구 댓글을 불러오는 함수 +function fetchCommentsForFootball(gameId) { + fetch(`/api/games/${gameId}/comments`) + .then(response => response.json()) + .then(comments => { + const commentsContainer = document.getElementById('comments-container'); + commentsContainer.innerHTML = ''; // 기존 내용 초기화 + + comments.forEach(comment => { + const commentElement = document.createElement('div'); + commentElement.className = 'comment-item'; + commentElement.innerHTML = ` +

${comment.memberName} (${formatDate(comment.createdDate)}): ${comment.content}

+ `; + commentsContainer.appendChild(commentElement); + }); + }) + .catch(error => console.error('Error:', error)); +} + +// 날짜 포맷 함수 (예: YYYY-MM-DD HH:mm 형식) +function formatDate(dateString) { + const date = new Date(dateString); + return date.toLocaleString(); +} + +// 페이지 로드 시 축구 경기 정보 및 댓글 불러오기 +document.addEventListener('DOMContentLoaded', function() { + const gameId = '특정 게임 ID'; // 실제 게임 ID로 대체 + fetchFootballGames(); + fetchCommentsForFootball(gameId); +}); diff --git a/src/main/resources/static/js/football.js b/src/main/resources/static/js/football.js new file mode 100644 index 00000000..87892e58 --- /dev/null +++ b/src/main/resources/static/js/football.js @@ -0,0 +1,56 @@ +// API 키와 호스트는 변수로 저장하여 재사용 +const apiKey = process.env.API_KEY; +const apiHostFootball = 'api-football-v1.p.rapidapi.com'; +const apiHostBasketball = 'api-basketball.p.rapidapi.com'; +const apiHostBaseball = 'api-baseball.p.rapidapi.com'; + +// 경기 정보를 가져오는 함수 +function fetchGames(sportType) { + let apiUrl, apiHost; + + switch (sportType) { + case 'football': + apiUrl = 'https://api-football-v1.p.rapidapi.com/v3/timezone'; + apiHost = apiHostFootball; + break; + case 'basketball': + apiUrl = 'https://api-basketball.p.rapidapi.com/timezone'; + apiHost = apiHostBasketball; + break; + case 'baseball': + apiUrl = 'https://api-baseball.p.rapidapi.com/timezone'; + apiHost = apiHostBaseball; + break; + default: + return; + } + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': apiKey, + 'X-RapidAPI-Host': apiHost + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 경기 일정을 페이지에 표시 + console.log(response); + }); +} + +// 축구 경기 정보 가져오기 +fetchGames('football').then(response => { + const footballScheduleContainer = document.getElementById('football-games-schedule'); + footballScheduleContainer.innerHTML = ''; // 기존 내용 초기화 + + // API 응답이 경기의 배열을 포함한다고 가정 + response.games.forEach(game => { + const gameInfo = document.createElement('div'); + gameInfo.innerHTML = ` +

${game.teamA} vs ${game.teamB}

+

Date: ${game.date}

+

Location: ${game.location}

+ `; + footballScheduleContainer.appendChild(gameInfo); + }); +}); \ No newline at end of file diff --git a/src/main/resources/static/js/index.js b/src/main/resources/static/js/index.js new file mode 100644 index 00000000..bb7b3e35 --- /dev/null +++ b/src/main/resources/static/js/index.js @@ -0,0 +1,42 @@ +// API 키와 호스트는 변수로 저장하여 재사용 +const apiKey = process.env.API_KEY; +const apiHostFootball = 'api-football-v1.p.rapidapi.com'; +const apiHostBasketball = 'api-basketball.p.rapidapi.com'; +const apiHostBaseball = 'api-baseball.p.rapidapi.com'; + +// 경기 정보를 가져오는 함수 +function fetchGames(sportType) { + let apiUrl, apiHost; + + switch (sportType) { + case 'football': + apiUrl = 'https://api-football-v1.p.rapidapi.com/v3/timezone'; + apiHost = apiHostFootball; + break; + case 'basketball': + apiUrl = 'https://api-basketball.p.rapidapi.com/timezone'; + apiHost = apiHostBasketball; + break; + case 'baseball': + apiUrl = 'https://api-baseball.p.rapidapi.com/timezone'; + apiHost = apiHostBaseball; + break; + default: + return; + } + + $.ajax({ + url: apiUrl, + method: 'GET', + headers: { + 'X-RapidAPI-Key': process.env.API_KEY, + 'X-RapidAPI-Host': apiHost + } + }).done(function(response) { + // 여기에서 response 데이터를 사용하여 경기 일정을 페이지에 표시 + console.log(response); + }); +} + +// 예시: 축구 경기 정보 가져오기 +fetchGames('football'); diff --git a/src/main/resources/templates/baseball-comment.html b/src/main/resources/templates/baseball-comment.html new file mode 100644 index 00000000..7474b2fa --- /dev/null +++ b/src/main/resources/templates/baseball-comment.html @@ -0,0 +1,58 @@ + + + + + EchoSports + + +
+ +

EchoSports

+
+ + +
+
+ + +
+
+

축구 경기 댓글

+ +
+ + +
+ +
+
+
+
+ +

© 내배캠 Team Echo

+
+ + + diff --git a/src/main/resources/templates/baseball.html b/src/main/resources/templates/baseball.html new file mode 100644 index 00000000..427ae32a --- /dev/null +++ b/src/main/resources/templates/baseball.html @@ -0,0 +1,58 @@ + + + + + EchoSports + + + + + +
+ +

EchoSports

+
+ + +
+
+ + +
+
+

야구 경기 일정

+
+
+ +
+ +
+ +

© 내배캠 Team Echo

+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/basketball-comments.html b/src/main/resources/templates/basketball-comments.html new file mode 100644 index 00000000..b309a44f --- /dev/null +++ b/src/main/resources/templates/basketball-comments.html @@ -0,0 +1,59 @@ + + + + + EchoSports + + +
+ +

EchoSports

+
+ + +
+
+ + + +
+
+

농구 경기 댓글

+
+ + +
+
+
+
+ +
+ +

© 내배캠 Team Echo

+
+ + + + diff --git a/src/main/resources/templates/basketball.html b/src/main/resources/templates/basketball.html new file mode 100644 index 00000000..94fa20ff --- /dev/null +++ b/src/main/resources/templates/basketball.html @@ -0,0 +1,58 @@ + + + + + EchoSports + + + + + +
+ +

EchoSports

+
+ + +
+
+ + +
+
+

농구 경기 일정

+
+
+ +
+ +
+ +

© 내배캠 Team Echo

+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/football-comment.html b/src/main/resources/templates/football-comment.html new file mode 100644 index 00000000..5b0d64e6 --- /dev/null +++ b/src/main/resources/templates/football-comment.html @@ -0,0 +1,58 @@ + + + + + EchoSports + + +
+ +

EchoSports

+
+ + +
+
+ + +
+
+

축구 경기 댓글

+ +
+ + +
+ +
+
+
+
+ +

© 내배캠 Team Echo

+
+ + + diff --git a/src/main/resources/templates/football.html b/src/main/resources/templates/football.html new file mode 100644 index 00000000..aa2d8f0b --- /dev/null +++ b/src/main/resources/templates/football.html @@ -0,0 +1,57 @@ + + + + + EchoSports + + + + + +
+ +

EchoSports

+
+ + +
+
+ + +
+
+

축구 경기 일정

+
+
+ +
+ +
+ +

© 내배캠 Team Echo

+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 00000000..67261dbe --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,59 @@ + + + + + EchoSports + + +
+ +

EchoSports

+
+ + +
+
+ + + + +
+
+

경기 일정

+
+
+
+ +
+ +
+ +

© 내배캠 Team Echo

+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/oauthLogin.html b/src/main/resources/templates/oauthLogin.html new file mode 100644 index 00000000..3182841c --- /dev/null +++ b/src/main/resources/templates/oauthLogin.html @@ -0,0 +1,89 @@ + + + + + 로그인 페이지 + + + + +
+
+
+
+

LOGIN

+

서비스 사용을 위해 로그인을 해주세요!

+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+
+
+
+ + +
+
+ + +
+ +
+
+
+
+
+ + diff --git a/src/main/resources/templates/signup.html b/src/main/resources/templates/signup.html new file mode 100644 index 00000000..063a2d68 --- /dev/null +++ b/src/main/resources/templates/signup.html @@ -0,0 +1,92 @@ + + + + + 회원 가입 + + + + + +
+
+
+
+

SIGN UP

+

서비스 사용을 위한 회원 가입

+ +
+
+ +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ + +
+
+ + +
+ + +
+
+
+
+
+
+ + \ No newline at end of file diff --git a/src/test/java/com/sportsecho/comment/CommentTestUtil.java b/src/test/java/com/sportsecho/comment/CommentTestUtil.java new file mode 100644 index 00000000..44f48513 --- /dev/null +++ b/src/test/java/com/sportsecho/comment/CommentTestUtil.java @@ -0,0 +1,15 @@ +package com.sportsecho.comment; + +import com.sportsecho.comment.entity.Comment; +import com.sportsecho.game.entity.Game; +import com.sportsecho.member.entity.Member; + +public class CommentTestUtil { + public static Comment createTestComment(String content, Game game, Member member) { + return Comment.builder() + .content(content) + .game(game) + .member(member) + .build(); + } +} diff --git a/src/test/java/com/sportsecho/comment/service/CommentServiceImplV1Test.java b/src/test/java/com/sportsecho/comment/service/CommentServiceImplV1Test.java new file mode 100644 index 00000000..a9880e40 --- /dev/null +++ b/src/test/java/com/sportsecho/comment/service/CommentServiceImplV1Test.java @@ -0,0 +1,186 @@ +package com.sportsecho.comment.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.sportsecho.comment.dto.CommentRequestDto; +import com.sportsecho.comment.dto.CommentResponseDto; +import com.sportsecho.comment.entity.Comment; +import com.sportsecho.comment.repository.CommentRepository; +import com.sportsecho.game.entity.Game; +import com.sportsecho.game.repository.GameRepository; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; +@ActiveProfiles("test") +class CommentServiceImplV1Test { + + @Mock + private CommentRepository commentRepository; + + @Mock + private GameRepository gameRepository; + + @Mock + private MemberRepository memberRepository; + + @InjectMocks + private CommentServiceImplV1 commentService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + @DisplayName("댓글 추가 테스트") + void testAddComment() { + // given + Long gameId = 1L; + String userEmail = "user@example.com"; + String testCommentContent = "Test comment"; + + CommentRequestDto commentDto = new CommentRequestDto(); + ReflectionTestUtils.setField(commentDto, "content", testCommentContent, String.class); + + Game game = new Game(); // Game 객체 생성 + when(gameRepository.findById(gameId)).thenReturn(Optional.of(game)); + + Member member = Member.builder() + .memberName("Test User") + .email(userEmail) + .password("TestPass123!") + .role(MemberRole.CUSTOMER) + .build(); + when(memberRepository.findByEmail(userEmail)).thenReturn(Optional.of(member)); + + // when + CommentResponseDto result = commentService.addComment(gameId, commentDto, userEmail); + + // then + assertNotNull(result); + assertEquals(commentDto.getContent(), result.getContent()); + verify(commentRepository, times(1)).save(any(Comment.class)); + } + @Test + @DisplayName("게임별 댓글 조회 테스트") + void testGetCommentsByGame() { + // given + Long gameId = 1L; // 테스트용 게임 ID + String userEmail = "user@example.com"; + Member testMember = Member.builder() + .memberName("Test User") + .email(userEmail) + .password("TestPass123!") + .role(MemberRole.CUSTOMER) + .build(); + + Comment comment1 = Comment.builder() + .id(1L) + .content("첫 번째 댓글") + .game(new Game()) + .member(testMember) + .build(); + + Comment comment2 = Comment.builder() + .id(2L) + .content("두 번째 댓글") + .game(new Game()) + .member(testMember) + .build(); + + List mockComments = Arrays.asList(comment1, comment2); + + when(commentRepository.findByGameId(gameId)).thenReturn(mockComments); + + // when + List result = commentService.getCommentsByGame(gameId); + + // then + assertNotNull(result); + assertEquals(2, result.size()); + assertEquals(mockComments.get(0).getContent(), result.get(0).getContent()); + assertEquals(mockComments.get(1).getContent(), result.get(1).getContent()); + } + @Test + @DisplayName("댓글 업데이트 테스트") + void testUpdateComment() { + // given + Long commentId = 1L; + String updatedContent = "업데이트된 댓글 내용"; + CommentRequestDto commentDto = new CommentRequestDto(); + ReflectionTestUtils.setField(commentDto, "content", updatedContent, String.class); + + String userEmail = "user@example.com"; + Member testMember = Member.builder() + .memberName("Test User") + .email(userEmail) + .password("TestPass123!") + .role(MemberRole.CUSTOMER) + .build(); + + Comment existingComment = Comment.builder() + .id(commentId) + .content("기존 댓글 내용") + .member(testMember) + .game(new Game()) // 적절한 Game 객체를 설정해야 함 + .build(); + + when(commentRepository.findById(commentId)).thenReturn(Optional.of(existingComment)); + when(memberRepository.findByEmail(userEmail)).thenReturn(Optional.of(testMember)); + + // when + CommentResponseDto result = commentService.updateComment(commentId, commentDto, userEmail); + + // then + assertNotNull(result); + assertEquals(updatedContent, result.getContent()); + assertEquals(commentId, result.getId()); + verify(commentRepository, times(1)).save(any(Comment.class)); + } + @Test + @DisplayName("댓글 삭제 테스트") + void testDeleteComment() { + // given + Long commentId = 1L; + String userEmail = "user@example.com"; + + Member testMember = Member.builder() + .memberName("Test User") + .email(userEmail) + .password("TestPass123!") + .role(MemberRole.CUSTOMER) + .build(); + + Comment existingComment = Comment.builder() + .id(commentId) + .content("기존 댓글 내용") + .member(testMember) + .game(new Game()) // 적절한 Game 객체 설정 + .build(); + + when(commentRepository.findById(commentId)).thenReturn(Optional.of(existingComment)); + when(memberRepository.findByEmail(userEmail)).thenReturn(Optional.of(testMember)); + + // when + commentService.deleteComment(commentId, userEmail); + + // then + verify(commentRepository, times(1)).delete(any(Comment.class)); + } +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/comment/service/CommentTestIntegration.java b/src/test/java/com/sportsecho/comment/service/CommentTestIntegration.java new file mode 100644 index 00000000..2f60ed1a --- /dev/null +++ b/src/test/java/com/sportsecho/comment/service/CommentTestIntegration.java @@ -0,0 +1,126 @@ +package com.sportsecho.comment.service; + +import static org.junit.jupiter.api.Assertions.*; + +import com.sportsecho.comment.dto.CommentRequestDto; +import com.sportsecho.comment.entity.Comment; +import com.sportsecho.comment.exception.CommentErrorCode; +import com.sportsecho.comment.repository.CommentRepository; +import com.sportsecho.game.GameTestUtil; +import com.sportsecho.game.entity.Game; +import com.sportsecho.game.repository.GameRepository; +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.MemberTestUtil; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +@ActiveProfiles("test") +@SpringBootTest +@Transactional +class CommentTestIntegration { + + @Autowired + private CommentService commentService; + + @Autowired + private CommentRepository commentRepository; + + @Autowired + private GameRepository gameRepository; + + @Autowired + private MemberRepository memberRepository; + + private Game testGame; + private Member testMember; + + @BeforeEach + void setUp() { + testMember = memberRepository.save( + MemberTestUtil.getTestMember("Test User", "user@example.com", "password", + MemberRole.CUSTOMER) + ); + + testGame = gameRepository.save( + GameTestUtil.createTestGame("Football", "Team A", "Team B", LocalDateTime.now(), + "Stadium") + ); + } + + @AfterEach + void tearDown() { + commentRepository.deleteAll(); + gameRepository.deleteAll(); + memberRepository.deleteAll(); + } + + @Test + @DisplayName("댓글 추가 테스트") + void testAddComment() { + CommentRequestDto commentDto = new CommentRequestDto("Test comment"); + + + commentService.addComment(testGame.getId(), commentDto, testMember.getEmail()); + + List comments = commentRepository.findByGameId(testGame.getId()); + assertFalse(comments.isEmpty()); + assertEquals("Test comment", comments.get(0).getContent()); + } + + @Test + @DisplayName("댓글 조회 테스트") + void testGetCommentsByGame() { + testAddComment(); + + List comments = commentRepository.findByGameId(testGame.getId()); + assertFalse(comments.isEmpty()); + } + + @Test + @DisplayName("댓글 수정 테스트") + void testUpdateComment() { + testAddComment(); + Comment existingComment = commentRepository.findByGameId(testGame.getId()).get(0); + CommentRequestDto updatedDto = new CommentRequestDto("Updated content"); + + + commentService.updateComment(existingComment.getId(), updatedDto, testMember.getEmail()); + + Comment updatedComment = commentRepository.findById(existingComment.getId()).orElseThrow(); + assertEquals("Updated content", updatedComment.getContent()); + } + + @Test + @DisplayName("댓글 삭제 테스트") + void testDeleteComment() { + testAddComment(); + + Comment existingComment = commentRepository.findByGameId(testGame.getId()).get(0); + commentService.deleteComment(existingComment.getId(), testMember.getEmail()); + + assertTrue(commentRepository.findById(existingComment.getId()).isEmpty()); + } + + @Test + @DisplayName("존재하지 않는 댓글 삭제 시도 테스트") + void testDeleteNonExistentComment() { + Long nonExistentCommentId = 999L; + + GlobalException exception = assertThrows(GlobalException.class, () -> + commentService.deleteComment(nonExistentCommentId, testMember.getEmail())); + + assertEquals(CommentErrorCode.COMMENT_NOT_FOUND, exception.getErrorCode()); + } +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/game/GameTestUtil.java b/src/test/java/com/sportsecho/game/GameTestUtil.java new file mode 100644 index 00000000..0c7b340c --- /dev/null +++ b/src/test/java/com/sportsecho/game/GameTestUtil.java @@ -0,0 +1,17 @@ +package com.sportsecho.game; + +import com.sportsecho.game.entity.Game; +import java.time.LocalDateTime; + +public class GameTestUtil { + public static Game createTestGame(String sportType, String teamA, String teamB, LocalDateTime dateTime, String location) { + return Game.builder() + .sportType(sportType) + .teamA(teamA) + .teamB(teamB) + .gameDateTime(dateTime) + .location(location) + .build(); + } + +} diff --git a/src/test/java/com/sportsecho/member/MemberIntegrationTest.java b/src/test/java/com/sportsecho/member/MemberIntegrationTest.java new file mode 100644 index 00000000..2bf9d7fe --- /dev/null +++ b/src/test/java/com/sportsecho/member/MemberIntegrationTest.java @@ -0,0 +1,305 @@ +package com.sportsecho.member; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sportsecho.common.jwt.JwtUtil; +import com.sportsecho.common.jwt.exception.JwtErrorCode; +import com.sportsecho.common.redis.RedisUtil; +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.dto.MemberRequestDto; +import com.sportsecho.member.dto.MemberResponseDto; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.exception.MemberErrorCode; +import com.sportsecho.member.repository.MemberRepository; +import com.sportsecho.member.service.MemberService; +import jakarta.persistence.EntityManager; +import java.util.Objects; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.mock.web.MockHttpServletRequest; +import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +public class MemberIntegrationTest implements MemberTest { + + @Autowired + @Qualifier("V2") + MemberService memberService; + + @Autowired + MemberRepository memberRepository; + + @Autowired + JwtUtil jwtUtil; + + @Autowired + RedisUtil redisUtil; + + @BeforeAll + void setUpAll() { + //member 회원가입을 통한 테스트 데이터 생성 - 회원가입 테스트는 Unit Test에서 진행 + memberService.signup(TEST_MEMBER_REQUEST_DTO, MemberRole.CUSTOMER); + } + + @Nested + @DisplayName("Member 로그인 테스트") + class memberLoginTest { + + MockHttpServletResponse response = new MockHttpServletResponse(); + + @Test + @DisplayName("Member 로그인 테스트 성공 - 토큰 반환") + void memberLogin_success() { + //given + + //when + memberService.login(TEST_MEMBER_REQUEST_DTO, response); + + String accessToken = response.getHeader(JwtUtil.AUTHORIZATION_HEADER); + String refreshToken = response.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //then + assertNotNull(accessToken); + assertNotNull(refreshToken); + } + + @Test + @DisplayName("Member 로그인 테스트 성공 - 토큰 이메일 검증") + void memberLogin_tokenValidate_success() { + //given + String email; + + //when + memberService.login(TEST_MEMBER_REQUEST_DTO, response); + + String accessToken = response.getHeader(JwtUtil.AUTHORIZATION_HEADER); + + if(accessToken != null) { + email = jwtUtil.getSubject(jwtUtil.substringToken(accessToken)); + } else { + throw new GlobalException(JwtErrorCode.ACCESS_TOKEN_NOT_FOUND); + } + + //then + assertEquals(TEST_MEMBER_REQUEST_DTO.getEmail(), email); + } + + @Test + @DisplayName("Member 로그인 테스트 성공 - 갱신 토큰 저장") + void memberLogin_refreshToken_create_success() { + //given + + //when + memberService.login(TEST_MEMBER_REQUEST_DTO, response); + + String refreshToken = response.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //then + assertTrue(redisUtil.isExist(refreshToken)); + } + + @Test + @DisplayName("Member 로그인 테스트 실패 - 존재하지 않는 이메일") + void memberLogin_fail_memberNotFound() { + //given + MemberRequestDto requestDto = MemberTestUtil.getTestMemberRequestDto( + ANOTHER_TEST_EMAIL, + TEST_MEMBER.getPassword() + ); + + //when + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.login(requestDto, response) + ); + + //then + assertEquals(MemberErrorCode.INVALID_AUTH, exception.getErrorCode()); + } + + @Test + @DisplayName("Member 로그인 테스트 실패 - 비밀번호 불일치") + void memberLogin_fail_invalidPassword() { + //given + MemberRequestDto requestDto = MemberTestUtil.getTestMemberRequestDto( + TEST_MEMBER.getEmail(), + ANOTHER_TEST_PASSWORD + ); + + //when + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.login(requestDto, response) + ); + + //then + assertEquals(MemberErrorCode.INVALID_AUTH, exception.getErrorCode()); + } + } + + @Nested + @DisplayName("Member 로그아웃 테스트") + class MemberLogoutTest { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + @BeforeEach + void MemberLogoutTestSetUp() { + memberService.login(TEST_MEMBER_REQUEST_DTO, response); + + request.addHeader(JwtUtil.AUTHORIZATION_HEADER, + Objects.requireNonNull(response.getHeader(JwtUtil.AUTHORIZATION_HEADER))); + + request.addHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER, + Objects.requireNonNull(response.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER))); + } + + @Test + @DisplayName("Member 로그아웃 테스트 성공 - 갱신 토큰 삭제") + void memberLogout_success() { + //given + String refreshToken = request.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //when + memberService.logout(TEST_MEMBER, request); + + //then + assertFalse(redisUtil.isExist(refreshToken)); + } + + @Test + @DisplayName("Member 로그아웃 테스트 실패 - 갱신 토큰이 존재하지 않는 경우") + void memberLogout_fail_refreshTokenNotFound() { + //given + String refreshToken = request.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //when + redisUtil.removeToken(refreshToken); + + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.logout(TEST_MEMBER, request) + ); + + //then + assertEquals(JwtErrorCode.REFRESH_TOKEN_NOT_FOUND, exception.getErrorCode()); + } + + @Test + @DisplayName("Member 로그아웃 테스트 실패 - 현재 접속중인 사용자가 로그아웃을 요청한 사용자가 아닌 경우") + void memberLogout_fail_invalidRequestMember() { + //given + Member anoterTestMember = MemberTestUtil.getTestMember( + ANOTHER_TEST_EMAIL, + TEST_MEMBER.getPassword() + ); + + //when + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.logout(anoterTestMember, request) + ); + + //then + assertEquals(MemberErrorCode.INVALID_REQUEST, exception.getErrorCode()); + } + } + + @Nested + @DisplayName("Member 접근 토큰 재발급 테스트") + class MemberRefreshTest { + + MockHttpServletRequest request = new MockHttpServletRequest(); + MockHttpServletResponse response = new MockHttpServletResponse(); + + @BeforeEach + void MemberRefreshSetUp() { + memberService.login(TEST_MEMBER_REQUEST_DTO, response); + + request.addHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER, + Objects.requireNonNull(response.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER))); + } + + @Test + @DisplayName("Member 접근 토큰 재발급 테스트 성공") + void memberRefresh_success() { + //given + + //when + memberService.refresh(request, response); + + String accessToken = response.getHeader(JwtUtil.AUTHORIZATION_HEADER); + String refreshToken = response.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //then + assertNotNull(accessToken); + assertNotNull(refreshToken); + } + + @Test + @DisplayName("Member 접근 토큰 재발급 테스트 실패 - 갱신 토큰이 존재하지 않는 경우") + void memberRefresh_fail_refreshTokenNotFound() { + //given + String refreshToken = request.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //when + redisUtil.removeToken(refreshToken); + + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.refresh(request, response) + ); + + //then + assertEquals(JwtErrorCode.REFRESH_TOKEN_NOT_FOUND, exception.getErrorCode()); + } + + @Test + @DisplayName("Member 접근 토큰 재발급 테스트 실패 - 사용자가 존재하지 않는 경우") + void memberRefresh_fail_memberNotFound() { + //given + String refreshToken = request.getHeader(JwtUtil.REFRESH_AUTHORIZATION_HEADER); + + //when + redisUtil.saveRefreshToken(refreshToken, ANOTHER_TEST_EMAIL); + + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.refresh(request, response) + ); + + //then + assertEquals(MemberErrorCode.USER_NOT_FOUND, exception.getErrorCode()); + } + } + + @Nested + @DisplayName("Member 회원탈퇴 테스트") + class MemberDeleteTest { + + @Test + @DisplayName("Member 회원탈퇴 테스트 성공") + void memberDelete_success() { + //given + + //when + MemberResponseDto memberResponseDto = memberService.deleteMember(TEST_MEMBER); + + //then + assertEquals(TEST_MEMBER.getEmail(), memberResponseDto.getEmail()); + } + } + +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/member/MemberTest.java b/src/test/java/com/sportsecho/member/MemberTest.java new file mode 100644 index 00000000..fd6a8884 --- /dev/null +++ b/src/test/java/com/sportsecho/member/MemberTest.java @@ -0,0 +1,28 @@ +package com.sportsecho.member; + +import com.sportsecho.member.dto.MemberRequestDto; +import com.sportsecho.member.entity.Member; + +public interface MemberTest { + + String TEST_MEMBER_NAME = "test_member_name"; + String TEST_EMAIL = "test_email@echo.com"; + String TEST_PASSWORD = "test_password1A~"; + + String ANOTHER_TEST_EMAIL = "test_email@echo.net"; + String ANOTHER_TEST_PASSWORD = "test_password1A!"; + + String TEST_REFRESH_TOKEN = "test_refresh_token"; + + Member TEST_MEMBER = Member.builder() + .memberName(TEST_MEMBER_NAME) + .email(TEST_EMAIL) + .password(TEST_PASSWORD) + .build(); + + MemberRequestDto TEST_MEMBER_REQUEST_DTO = MemberTestUtil.getTestMemberRequestDto( + TEST_MEMBER_NAME, + TEST_EMAIL, + TEST_PASSWORD + ); +} diff --git a/src/test/java/com/sportsecho/member/MemberTestUtil.java b/src/test/java/com/sportsecho/member/MemberTestUtil.java new file mode 100644 index 00000000..65354346 --- /dev/null +++ b/src/test/java/com/sportsecho/member/MemberTestUtil.java @@ -0,0 +1,36 @@ +package com.sportsecho.member; + +import com.sportsecho.member.dto.MemberRequestDto; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import org.springframework.test.util.ReflectionTestUtils; + +public class MemberTestUtil implements MemberTest { + + public static Member getTestMember(String memberName, String email, String password, MemberRole role) { + return Member.builder() + .memberName(memberName) + .email(email) + .password(password) + .role(role) + .build(); + } + + public static Member getTestMember(String email, String password) { + return getTestMember(TEST_MEMBER_NAME, email, password, MemberRole.CUSTOMER); + } + + public static MemberRequestDto getTestMemberRequestDto(String memberName, String email, String password) { + MemberRequestDto memberRequestDto = new MemberRequestDto(); + + ReflectionTestUtils.setField(memberRequestDto, "memberName", memberName, String.class); + ReflectionTestUtils.setField(memberRequestDto, "email", email, String.class); + ReflectionTestUtils.setField(memberRequestDto, "password", password, String.class); + + return memberRequestDto; + } + + public static MemberRequestDto getTestMemberRequestDto(String email, String password) { + return getTestMemberRequestDto(TEST_MEMBER_NAME, email, password); + } +} diff --git a/src/test/java/com/sportsecho/member/service/MemberServiceTest.java b/src/test/java/com/sportsecho/member/service/MemberServiceTest.java new file mode 100644 index 00000000..99e0c159 --- /dev/null +++ b/src/test/java/com/sportsecho/member/service/MemberServiceTest.java @@ -0,0 +1,85 @@ +package com.sportsecho.member.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.BDDMockito.given; + +import com.sportsecho.common.jwt.JwtUtil; +import com.sportsecho.common.oauth.OAuthUtil; +import com.sportsecho.common.redis.RedisUtil; +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.MemberTest; +import com.sportsecho.member.dto.MemberResponseDto; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.exception.MemberErrorCode; +import com.sportsecho.member.repository.MemberRepository; +import java.util.Optional; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.crypto.password.PasswordEncoder; + +@ExtendWith(MockitoExtension.class) +class MemberServiceTest implements MemberTest { + + @InjectMocks + MemberServiceImplV2 memberService; + + @Mock + MemberRepository memberRepository; + + @Mock + JwtUtil jwtUtil; + + @Mock + OAuthUtil oAuthUtil; + + @Mock + RedisUtil redisUtil; + + @Mock + AuthenticationManager authenticationManager; + + @Mock + PasswordEncoder passwordEncoder; + + @Nested + @DisplayName("Member 회원가입 테스트") + class memberSignupTest { + + @Test + @DisplayName("Member 회원가입 테스트 성공") + void memberSignup_success() { + //given + given(memberRepository.findByEmail(any())).willReturn(Optional.empty()); + + //when + MemberResponseDto response = memberService.signup(TEST_MEMBER_REQUEST_DTO, MemberRole.CUSTOMER); + + //then + assertEquals(TEST_MEMBER_REQUEST_DTO.getMemberName(), response.getMemberName()); + assertEquals(TEST_MEMBER_REQUEST_DTO.getEmail(), response.getEmail()); + } + + @Test + @DisplayName("Member 회원가입 테스트 실패 - 회원가입 요청 이메일 중복") + void memberSignup_fail_duplicate_email() { + //given + given(memberRepository.findByEmail(any())).willReturn(Optional.of(TEST_MEMBER)); + + //when + GlobalException exception = assertThrows(GlobalException.class, () -> + memberService.signup(TEST_MEMBER_REQUEST_DTO, MemberRole.CUSTOMER) + ); + + //then + assertEquals(MemberErrorCode.DUPLICATED_EMAIL, exception.getErrorCode()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/memberProduct/service/MemberProductServiceImplV1IntTest.java b/src/test/java/com/sportsecho/memberProduct/service/MemberProductServiceImplV1IntTest.java new file mode 100644 index 00000000..00a56c3f --- /dev/null +++ b/src/test/java/com/sportsecho/memberProduct/service/MemberProductServiceImplV1IntTest.java @@ -0,0 +1,185 @@ +package com.sportsecho.memberProduct.service; + +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import com.sportsecho.memberProduct.dto.MemberProductRequestDto; +import com.sportsecho.memberProduct.dto.MemberProductResponseDto; +import com.sportsecho.memberProduct.entity.MemberProduct; +import com.sportsecho.memberProduct.exception.MemberProductErrorCode; +import com.sportsecho.memberProduct.repository.MemberProductRepository; +import com.sportsecho.product.entity.Product; +import com.sportsecho.product.repository.ProductRepository; +import org.junit.jupiter.api.*; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.test.util.ReflectionTestUtils; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +@ActiveProfiles("test") +@SpringBootTest +class MemberProductServiceImplV1IntTest { + + @Autowired + MemberProductRepository memberProductRepository; + + @Autowired + ProductRepository productRepository; + + @Autowired + MemberRepository memberRepository; + + @Autowired + MemberProductServiceImplV1 memberProductService; + + Member member; + Product product; + MemberProductRequestDto requestDto = new MemberProductRequestDto(); + + @BeforeEach + void setUp() { + member = Member.builder() + .memberName("name") + .email("member@email.com") + .password("password") + .role(MemberRole.CUSTOMER) + .build(); + product = Product.builder() + .title("상품") + .content("설명") + .imageUrl("test image") + .price(10000) + .quantity(100) + .build(); + + ReflectionTestUtils.setField(requestDto, "productsQuantity", 3); + + memberRepository.save(member); + productRepository.save(product); + } + + @AfterEach + void tearDown() { + memberProductRepository.deleteAll(); + productRepository.deleteAll(); + memberRepository.deleteAll(); + } + + private MemberProduct createMemberProduct() { + MemberProduct memberProduct = MemberProduct.builder() + .member(member) + .product(product) + .productsQuantity(2) + .build(); + memberProductRepository.save(memberProduct); + return memberProduct; + } + + @Nested + @DisplayName("장바구니에 상품 추가 테스트") + class addCartTest { + @Test + @DisplayName("장바구니 추가 성공 - 새 상품") + void addCartTest_success_new() { + //when + MemberProductResponseDto responseDto = memberProductService.addCart(product.getId(), requestDto, member); + + //then + assertEquals(requestDto.getProductsQuantity(), responseDto.getProductsQuantity()); + assertEquals(product.getPrice(), responseDto.getPrice()); + assertEquals(product.getTitle(), responseDto.getTitle()); + } + + @Test + @DisplayName("장바구니 추가 성공 - 기존에 존재하던 상품") + void addCartTest_success_old() { + //given + MemberProduct memberProduct = createMemberProduct(); + + //when + MemberProductResponseDto responseDto = memberProductService.addCart(product.getId(), requestDto, member); + + //then + assertEquals(requestDto.getProductsQuantity() + memberProduct.getProductsQuantity(), + responseDto.getProductsQuantity()); + assertEquals(product.getPrice(), responseDto.getPrice()); + assertEquals(product.getTitle(), responseDto.getTitle()); + } + + @Test + @DisplayName("장바구니 추가 실패 - 상품이 존재하지 않음") + void addCartTest_fail_notFoundProduct() { + //when - then + GlobalException e = assertThrows(GlobalException.class, () -> { + memberProductService.addCart(10L, requestDto, member); + }); + assertEquals(MemberProductErrorCode.NOT_FOUND_PRODUCT, e.getErrorCode()); + } + } + + @Test + @DisplayName("장바구니 조회 테스트") + void Test() { + //given + MemberProduct memberProduct1 = createMemberProduct(); + MemberProduct memberProduct2 = createMemberProduct(); + MemberProduct memberProduct3 = createMemberProduct(); + + //when + List memberProductList = memberProductService.getCart(member); + + //then + assertEquals(3, memberProductList.size()); + assertEquals(memberProduct1.getProduct().getTitle(), memberProductList.get(0).getTitle()); + } + + @Nested + @DisplayName("장바구니 삭제 테스트") + class deleteCart { + @Test + @DisplayName("단일 상품 삭제 성공") + void deleteCartTest_success() { + //given + MemberProduct memberProduct = createMemberProduct(); + + //when + memberProductService.deleteCart(product.getId(), member); + + //then + assertTrue(memberProductRepository.findById(memberProduct.getId()).isEmpty()); + } + + @Test + @DisplayName("단일 상품 삭제 실패 - 장바구니에 상품이 없음") + void deleteCartTest_fail() { + //when - then + GlobalException e = assertThrows(GlobalException.class, () -> { + memberProductService.deleteCart(product.getId(), member); + + }); + assertEquals(MemberProductErrorCode.NOT_FOUND_PRODUCT_IN_CART, e.getErrorCode()); + } + + @Test + @DisplayName("전체 상품 삭제 성공") + void deleteAllCartTest_success() { + //given + MemberProduct memberProduct1 = createMemberProduct(); + MemberProduct memberProduct2 = createMemberProduct(); + MemberProduct memberProduct3 = createMemberProduct(); + + //when + memberProductService.deleteAllCart(member); + + //then + assertTrue(memberProductRepository.findById(memberProduct1.getId()).isEmpty()); + assertTrue(memberProductRepository.findById(memberProduct2.getId()).isEmpty()); + assertTrue(memberProductRepository.findById(memberProduct3.getId()).isEmpty()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/product/ProductTest.java b/src/test/java/com/sportsecho/product/ProductTest.java new file mode 100644 index 00000000..a820fb2e --- /dev/null +++ b/src/test/java/com/sportsecho/product/ProductTest.java @@ -0,0 +1,20 @@ +package com.sportsecho.product; + +import com.sportsecho.product.entity.Product; + +public interface ProductTest { + + String TEST_PRODUCT_TITLE = "test_product_title"; + String TEST_PRODUCT_CONTENT = "test_product_content"; + String TEST_PRODUCT_IMAGEURL = "testimageurl.com"; + int TEST_PRICE = 9900; + int TEST_QUANTITY = 10; + + Product TEST_PRODUCT = Product.builder() + .title(TEST_PRODUCT_TITLE) + .content(TEST_PRODUCT_CONTENT) + .imageUrl(TEST_PRODUCT_IMAGEURL) + .price(TEST_PRICE) + .quantity(TEST_QUANTITY) + .build(); +} diff --git a/src/test/java/com/sportsecho/product/ProductTestUtil.java b/src/test/java/com/sportsecho/product/ProductTestUtil.java new file mode 100644 index 00000000..3f7e7a6f --- /dev/null +++ b/src/test/java/com/sportsecho/product/ProductTestUtil.java @@ -0,0 +1,31 @@ +package com.sportsecho.product; + +import com.sportsecho.product.dto.request.ProductRequestDto; +import com.sportsecho.product.entity.Product; +import org.springframework.test.util.ReflectionTestUtils; + +public class ProductTestUtil implements ProductTest{ + + public static Product getTestProduct() { + return Product.builder() + .title(TEST_PRODUCT_TITLE) + .content(TEST_PRODUCT_CONTENT) + .imageUrl(TEST_PRODUCT_IMAGEURL) + .price(TEST_PRICE) + .quantity(TEST_QUANTITY) + .build(); + } + + public static ProductRequestDto createTestProductRequestDto(String productTitle, + String productContent, String productImageUrl, int productPrice, int productQuantity) { + ProductRequestDto productRequestDto = new ProductRequestDto(); + + ReflectionTestUtils.setField(productRequestDto, "title", productTitle, String.class); + ReflectionTestUtils.setField(productRequestDto, "content", productContent, String.class); + ReflectionTestUtils.setField(productRequestDto, "imageUrl", productImageUrl, String.class); + ReflectionTestUtils.setField(productRequestDto, "price", productPrice, int.class); + ReflectionTestUtils.setField(productRequestDto, "quantity", productQuantity, int.class); + return productRequestDto; + } + +} diff --git a/src/test/java/com/sportsecho/product/service/ProductServiceImplV1TestIntegration.java b/src/test/java/com/sportsecho/product/service/ProductServiceImplV1TestIntegration.java new file mode 100644 index 00000000..ed3fe674 --- /dev/null +++ b/src/test/java/com/sportsecho/product/service/ProductServiceImplV1TestIntegration.java @@ -0,0 +1,207 @@ +package com.sportsecho.product.service; + +import static org.mockito.Mockito.*; +import static org.junit.jupiter.api.Assertions.*; + +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.MemberTest; +import com.sportsecho.member.MemberTestUtil; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import com.sportsecho.product.ProductTest; +import com.sportsecho.product.ProductTestUtil; +import com.sportsecho.product.dto.request.ProductRequestDto; +import com.sportsecho.product.dto.response.ProductResponseDto; +import com.sportsecho.product.entity.Product; +import com.sportsecho.product.exception.ProductErrorCode; +import com.sportsecho.product.repository.ProductRepository; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class ProductServiceImplV1TestIntegration implements MemberTest, ProductTest { + + @Autowired + private ProductService productService; + + @Autowired + private MemberRepository memberRepository; + + @Autowired + private ProductRepository productRepository; + + private Member adminMember, customerMember; + private Product product; + private ProductRequestDto requestDto; + + + @BeforeEach + void setUp() { + customerMember = memberRepository.save( + MemberTestUtil.getTestMember("customer", "customer@email.com", "Customer123@", + MemberRole.CUSTOMER)); + adminMember = memberRepository.save( + MemberTestUtil.getTestMember("admin", "admin@email.com", "Admin123@", + MemberRole.ADMIN)); + product = ProductTestUtil.getTestProduct(); + requestDto = ProductTestUtil.createTestProductRequestDto("제목", "내용", + "image.com", 3000, 10); + } + + @AfterEach + void tearDown() { + memberRepository.deleteAll(); + productRepository.deleteAll(); + } + + @Nested + @DisplayName("상품 생성 테스트") + class ProductSaveTest { + + @Test + @DisplayName("성공 - 상품 생성") + void createProductSuccess() { +// productService.createProduct(requestDto, adminMember); + assertNotNull(productRepository); + } + + @Test + @DisplayName("실패 - 권한 없음(일반유저)") + void createProductFail_NOAUTHORIZATION() { + assertThrows(GlobalException.class, () -> { + productService.createProduct(requestDto, customerMember); + }); + } + } + + @Nested + @DisplayName("상품 조회 테스트") + class GetProductTest { + + @Nested + @DisplayName("단건 조회 테스트") + class getSingleProductTest { + + @Test + @DisplayName("성공 - 상품 단건 조회") + void getProductSuccess() { + productRepository.save(product); + + ProductResponseDto result = productService.getProduct(product.getId()); + assertNotNull(result); + assertEquals(result.getTitle(), product.getTitle()); + assertEquals(result.getContent(), product.getContent()); + assertEquals(result.getImageUrl(), product.getImageUrl()); + assertEquals(result.getPrice(), product.getPrice()); + assertEquals(result.getQuantity(), product.getQuantity()); + } + + @Test + @DisplayName("실패 - 존재하지 않는 상품") + void getProductFail_NOTFOUNDPRODUCT() { + Long productId = 99L; + GlobalException thrown = assertThrows(GlobalException.class, () -> { + productService.getProduct(productId); + }); + assertEquals(ProductErrorCode.NOT_FOUND_PRODUCT, thrown.getErrorCode()); + } + } + + @Nested + @DisplayName("목록 조회 테스트") + class getProductListTest { + + @BeforeEach + void setProductList() { + for (int i = 0; i < 10; i++) { + productRepository.save(Product.builder() + .title(i + "번 상품 제목") + .content(i + "번 상품내용") + .imageUrl("image.com") + .price(i * 1000) + .quantity(i * 100) + .build()); + } + } + + @Test + @DisplayName("성공 - 상품 목록 조회(가격기준 오름차순)") + void getProductListSuccess() { + Sort sort = Sort.by(Sort.Direction.fromString("asc"), "price"); + int pageSize = 5; + Pageable pageable = PageRequest.of(0, pageSize, sort); + List allPrices = productRepository.findAll(Sort.by("price")).stream() + .map(Product::getPrice) + .toList(); + + List productList = productService.getProductListWithPagiNation( + pageable); + assertEquals(pageSize, productList.size()); + assertEquals(productList.get(0).getPrice(), allPrices.get(0)); + } + } + } + + @Nested + @DisplayName("상품 수정 테스트") + class updateProductTest { + + @Test + @DisplayName("성공 - 상품 수정") + void updateProductSuccess() { + productRepository.save(product); + ProductResponseDto responseDto = productService.updateProduct(adminMember, + product.getId(), requestDto); + + assertEquals(requestDto.getTitle(), responseDto.getTitle()); + assertEquals(requestDto.getContent(), responseDto.getContent()); + assertEquals(requestDto.getImageUrl(), responseDto.getImageUrl()); + assertEquals(requestDto.getPrice(), responseDto.getPrice()); + assertEquals(requestDto.getQuantity(), responseDto.getQuantity()); + } + + @Test + @DisplayName("실패 - 권한 없음(일반유저)") + void updateProductFail_NOAUTHORIZATION() { + GlobalException thrown = assertThrows(GlobalException.class, () -> { + productService.updateProduct(customerMember, product.getId(), requestDto); + }); + assertEquals(ProductErrorCode.NO_AUTHORIZATION, thrown.getErrorCode()); + } + } + + @Nested + @DisplayName("상품 삭제 테스트") + class deleteProductTest { + + @Test + @DisplayName("성공 - 상품 삭제") + void deleteProductSuccess() { + productRepository.save(product); + productService.deleteProduct(adminMember, product.getId()); + assertTrue(productRepository.findById(product.getId()).isEmpty()); + } + + @Test + @DisplayName("실패 - 존재하지 않는 상품") + void deleteProductFail_NOTFOUNDPRODUCT() { + Long productId = 99L; + GlobalException thrown = assertThrows(GlobalException.class, () -> { + productService.deleteProduct(adminMember, productId); + }); + assertEquals(ProductErrorCode.NOT_FOUND_PRODUCT, thrown.getErrorCode()); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/sportsecho/purchase/service/PurchaseServiceImplV1Test.java b/src/test/java/com/sportsecho/purchase/service/PurchaseServiceImplV1Test.java new file mode 100644 index 00000000..abf545d4 --- /dev/null +++ b/src/test/java/com/sportsecho/purchase/service/PurchaseServiceImplV1Test.java @@ -0,0 +1,163 @@ +package com.sportsecho.purchase.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.sportsecho.global.exception.GlobalException; +import com.sportsecho.member.entity.Member; +import com.sportsecho.member.entity.MemberRole; +import com.sportsecho.member.repository.MemberRepository; +import com.sportsecho.memberProduct.entity.MemberProduct; +import com.sportsecho.memberProduct.repository.MemberProductRepository; +import com.sportsecho.product.entity.Product; +import com.sportsecho.product.repository.ProductRepository; +import com.sportsecho.purchase.dto.PurchaseRequestDto; +import com.sportsecho.purchase.dto.PurchaseResponseDto; +import com.sportsecho.purchase.exception.PurchaseErrorCode; +import com.sportsecho.purchase.repository.PurchaseRepository; +import com.sportsecho.purchaseProduct.repository.PurchaseProductRepository; +import java.util.List; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +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 org.springframework.test.util.ReflectionTestUtils; + +@ActiveProfiles("test") +@SpringBootTest +class PurchaseServiceImplV1Test { + + @Autowired + MemberRepository memberRepository; + + @Autowired + ProductRepository productRepository; + + @Autowired + PurchaseRepository purchaseRepository; + + @Autowired + PurchaseProductRepository purchaseProductRepository; + + @Autowired + MemberProductRepository memberProductRepository; + + @Autowired + PurchaseServiceImplV1 purchaseService; + + Member member; + Product product; + MemberProduct memberProduct; + PurchaseRequestDto requestDto = new PurchaseRequestDto(); + + @BeforeEach + void setUp() { + member = Member.builder() + .memberName("name") + .email("member@email.com") + .password("password") + .role(MemberRole.CUSTOMER) + .build(); + product = Product.builder() + .title("상품") + .content("설명") + .imageUrl("test image") + .price(10000) + .quantity(100) + .build(); + memberProduct = MemberProduct.builder() + .member(member) + .product(product) + .productsQuantity(2) + .build(); + + ReflectionTestUtils.setField(requestDto, "address", "스포츠시 에코동"); + ReflectionTestUtils.setField(requestDto, "phone", "010-1234-5678"); + + memberRepository.save(member); + productRepository.save(product); + memberProductRepository.save(memberProduct); + } + + @AfterEach + void tearDown() { + memberProductRepository.deleteAll(); + purchaseProductRepository.deleteAll(); + purchaseRepository.deleteAll(); + productRepository.deleteAll(); + memberRepository.deleteAll(); + } + + @Nested + @DisplayName("구매 테스트") + class purchaseTest { + + @Test + @DisplayName("장바구니에 있던 상품 구매 성공") + void purchaseTest_success() { + //when + PurchaseResponseDto responseDto = purchaseService.purchase(requestDto, member); + + //then + assertEquals(product.getPrice() * memberProduct.getProductsQuantity(), + responseDto.getTotalPrice()); + assertEquals(requestDto.getAddress(), responseDto.getAddress()); + assertEquals(product.getTitle(), responseDto.getResponseDtoList().get(0).getTitle()); + assertTrue(memberProductRepository.findByMemberId(member.getId()).isEmpty()); + } + + @Test + @DisplayName("구매 실패 - 장바구니가 비어있음") + void purchaseTest_fail() { + //given + memberProductRepository.deleteAll(); + + //when - then + GlobalException e = assertThrows(GlobalException.class, () -> { + purchaseService.purchase(requestDto, member); + }); + assertEquals(PurchaseErrorCode.EMPTY_CART, e.getErrorCode()); + } + } + + @Nested + @DisplayName("구매 목록 조회 테스트") + class getPurchaseListTest { + + @Test + @DisplayName("멤버의 구매 목록 조회 성공") + void getPurchaseListTest_success() { + //given + purchaseService.purchase(requestDto, member); + + //when + List responseDtoList = purchaseService.getPurchaseList(member); + + //then + assertEquals(1, responseDtoList.size()); + assertEquals(requestDto.getAddress(), responseDtoList.get(0).getAddress()); + assertEquals(memberProduct.getProductsQuantity() * product.getPrice(), + responseDtoList.get(0).getTotalPrice()); + assertEquals(product.getTitle(), + responseDtoList.get(0).getResponseDtoList().get(0).getTitle()); + assertEquals(memberProduct.getProductsQuantity(), + responseDtoList.get(0).getResponseDtoList().get(0).getProductsQuantity()); + } + + @Test + @DisplayName("멤버의 구매 목록 조회 실패 - 구매 내역 없음") + void getPurchaseListTest_fail() { + //when - then + GlobalException e = assertThrows(GlobalException.class, () -> { + purchaseService.getPurchaseList(member); + }); + assertEquals(PurchaseErrorCode.EMPTY_PURCHASE_LIST, e.getErrorCode()); + } + } + +} \ No newline at end of file