From dd13fb83f0f6a3c675d13c43ebcde2b2008f6654 Mon Sep 17 00:00:00 2001 From: yoon_je Date: Fri, 20 Jun 2025 10:32:56 +0900 Subject: [PATCH 01/18] =?UTF-8?q?update:=20=EB=A1=9C=EA=B7=B8=EC=95=84?= =?UTF-8?q?=EC=9B=83=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A9=94=EC=84=9C?= =?UTF-8?q?=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/ureca/uplait/domain/auth/service/AuthService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java b/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java index 4467d6b..e5ea123 100644 --- a/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java +++ b/src/main/java/com/ureca/uplait/domain/auth/service/AuthService.java @@ -2,6 +2,7 @@ import java.time.LocalDateTime; +import jakarta.transaction.Transactional; import org.springframework.stereotype.Service; import com.ureca.uplait.domain.auth.api.KakaoOauthClient; @@ -81,6 +82,7 @@ public void reissue(String refreshToken, HttpServletResponse response){ jwtProvider.addRefreshTokenCookie(response, newRefreshToken); } + @Transactional public void logout(String refreshToken, HttpServletResponse response){ if (!jwtValidator.validateToken(refreshToken)) { From 3aa3acb03be17ae7ca07704b48bf4298bef10fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EB=8F=99=EC=A4=80?= <127932430+djlim00@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:21:41 +0900 Subject: [PATCH 02/18] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4cb146..97728f7 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요금제를 비교, 추천 받을 수 있습니다. -### [프론트엔드 레포지토리 바로가기](https://github.com/U-plait/u-plait-fe) -### [AI 레포지토리 바로가기](https://github.com/U-plait/u-plait-ai) +### [U-plait 프론트엔드 레포지토리 바로가기](https://github.com/U-plait/u-plait-fe) +### [U-plait AI 레포지토리 바로가기](https://github.com/U-plait/u-plait-ai)

# 1. 프로젝트의 배경 ### 1.1 문제인식 From 844bb52ece9a66055a6e1ac07ecd9c457bc79b46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EC=9E=84=EB=8F=99=EC=A4=80?= <127932430+djlim00@users.noreply.github.com> Date: Fri, 20 Jun 2025 13:23:19 +0900 Subject: [PATCH 03/18] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 97728f7..619cae5 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,8 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요금제를 비교, 추천 받을 수 있습니다. -### [U-plait 프론트엔드 레포지토리 바로가기](https://github.com/U-plait/u-plait-fe) -### [U-plait AI 레포지토리 바로가기](https://github.com/U-plait/u-plait-ai) +### [U-plait : 프론트엔드 레포지토리 바로가기](https://github.com/U-plait/u-plait-fe) +### [U-plait : AI 레포지토리 바로가기](https://github.com/U-plait/u-plait-ai)

# 1. 프로젝트의 배경 ### 1.1 문제인식 @@ -158,7 +158,7 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요 - ERD
![Image](https://github.com/user-attachments/assets/271fbd65-50ba-4528-a96c-3ae0b16a7d34) - +

# 6. 기대 효과 From 9c3df8b4cbc6b1fb14731d009e536f86dd0d947e Mon Sep 17 00:00:00 2001 From: yoon_je Date: Fri, 20 Jun 2025 14:06:48 +0900 Subject: [PATCH 04/18] =?UTF-8?q?fix:=20=EC=A0=84=ED=99=94=EB=B2=88?= =?UTF-8?q?=ED=98=B8,=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A4=91=EB=B3=B5?= =?UTF-8?q?=EA=B2=80=EC=82=AC=20=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95,?= =?UTF-8?q?=20=EA=B7=B8=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95=20=EB=B0=8F?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserController.java | 22 +++++------ .../domain/user/service/UserService.java | 11 ++++-- .../admin/service/AdminReviewServiceTest.java | 11 +++--- .../domain/user/service/UserServiceTest.java | 39 ++++++++++++++++--- 4 files changed, 57 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/user/controller/UserController.java b/src/main/java/com/ureca/uplait/domain/user/controller/UserController.java index 4e07291..283f458 100644 --- a/src/main/java/com/ureca/uplait/domain/user/controller/UserController.java +++ b/src/main/java/com/ureca/uplait/domain/user/controller/UserController.java @@ -44,24 +44,24 @@ public CommonResponse addTag(@RequestBody AddTagRequest request, @Authenti } @GetMapping("/duplicate/phone") - @Operation(summary="전화번호 중복검사 API", description = "추가정보 입력 시 전화번호 중복 검사를 수행한다.") + @Operation(summary = "전화번호 중복검사 API", description = "추가정보 입력 시 전화번호 중복 검사를 수행한다.") public CommonResponse duplicatePhone( - @RequestParam("value") - @Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "전화번호는 010-xxxx-xxxx 형식이어야 합니다.") - String phoneNumber) { - boolean duplicated = userService.isPhoneNumberDuplicated(phoneNumber); + @RequestParam("value") + @Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "전화번호는 010-xxxx-xxxx 형식이어야 합니다.") String phoneNumber, + @RequestParam(value = "current", required = false) String currentPhoneNumber + ) { + boolean duplicated = userService.isPhoneNumberDuplicated(phoneNumber, currentPhoneNumber); DuplicateResponse result = new DuplicateResponse(duplicated); return CommonResponse.success(result); } @GetMapping("/duplicate/email") - @Operation(summary="이메일 중복검사 API", description = "추가정보 입력 시 이메일 중복 검사를 수행한다.") + @Operation(summary = "이메일 중복검사 API", description = "추가정보 입력 시 이메일 중복 검사를 수행한다.") public CommonResponse duplicateEmail( - @RequestParam("value") - @Email(message = "올바른 이메일 형식이 아닙니다.") - String email - ){ - boolean duplicated = userService.isEmailDuplicated(email); + @RequestParam("value") @Email(message = "올바른 이메일 형식이 아닙니다.") String email, + @RequestParam(value = "current", required = false) String currentEmail + ) { + boolean duplicated = userService.isEmailDuplicated(email, currentEmail); DuplicateResponse result = new DuplicateResponse(duplicated); return CommonResponse.success(result); } diff --git a/src/main/java/com/ureca/uplait/domain/user/service/UserService.java b/src/main/java/com/ureca/uplait/domain/user/service/UserService.java index de3cf00..5c4ab6c 100644 --- a/src/main/java/com/ureca/uplait/domain/user/service/UserService.java +++ b/src/main/java/com/ureca/uplait/domain/user/service/UserService.java @@ -51,12 +51,17 @@ public void addUserTag(AddTagRequest request, User user) { } } - public boolean isPhoneNumberDuplicated( - @Pattern(regexp = "^010-\\d{4}-\\d{4}$", message = "전화번호는 010-xxxx-xxxx 형식이어야 합니다.") String phoneNumber) { + public boolean isPhoneNumberDuplicated(String phoneNumber, String currentPhoneNumber) { + if (phoneNumber.equals(currentPhoneNumber)) { + return false; + } return userRepository.existsByPhoneNumber(phoneNumber); } - public boolean isEmailDuplicated(@Email(message = "올바른 이메일 형식이 아닙니다.") String email) { + public boolean isEmailDuplicated(String email, String currentEmail) { + if (email.equals(currentEmail)) { + return false; + } return userRepository.existsByEmail(email); } } diff --git a/src/test/java/com/ureca/uplait/domain/admin/service/AdminReviewServiceTest.java b/src/test/java/com/ureca/uplait/domain/admin/service/AdminReviewServiceTest.java index 522b391..961e705 100644 --- a/src/test/java/com/ureca/uplait/domain/admin/service/AdminReviewServiceTest.java +++ b/src/test/java/com/ureca/uplait/domain/admin/service/AdminReviewServiceTest.java @@ -1,5 +1,6 @@ package com.ureca.uplait.domain.admin.service; +import com.ureca.uplait.domain.admin.dto.response.AdminDetailReviewResponse; import com.ureca.uplait.domain.admin.dto.response.AdminReviewDeleteResponse; import com.ureca.uplait.domain.admin.dto.response.AdminReviewResponse; import com.ureca.uplait.domain.plan.entity.MobilePlan; @@ -103,16 +104,14 @@ void getAllReviewsForAdmin() { review1.getUser().getName(), review1.getTitle(), review1.getRating(), - "25.06.08", - review1.getContent() + LocalDateTime.now() ); AdminReviewResponse response2 = new AdminReviewResponse( review2.getId(), review2.getUser().getName(), review2.getTitle(), review2.getRating(), - "25.06.09", - review2.getContent() + LocalDateTime.now() ); Page page = new PageImpl<>(List.of(response1, response2)); @@ -138,7 +137,7 @@ void getReviewDetailForAdmin() { when(reviewRepository.findById(user.getId())).thenReturn(Optional.of(review1)); //when - AdminReviewResponse response = adminReviewService.getReviewDetailForAdmin(user.getId()); + AdminDetailReviewResponse response = adminReviewService.getReviewDetailForAdmin(user.getId(), user); //then assertEquals(1L, response.getReviewId()); @@ -157,7 +156,7 @@ void getReviewDetailForAdmin_Exception() { //when & then GlobalException exception = assertThrows(GlobalException.class, () -> { - adminReviewService.getReviewDetailForAdmin(review1.getId()); + adminReviewService.getReviewDetailForAdmin(review1.getId(), user); }); assertEquals(ResultCode.REVIEW_NOT_FOUND, exception.getResultCode()); diff --git a/src/test/java/com/ureca/uplait/domain/user/service/UserServiceTest.java b/src/test/java/com/ureca/uplait/domain/user/service/UserServiceTest.java index 8366233..dc36e08 100644 --- a/src/test/java/com/ureca/uplait/domain/user/service/UserServiceTest.java +++ b/src/test/java/com/ureca/uplait/domain/user/service/UserServiceTest.java @@ -2,8 +2,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.*; import com.ureca.uplait.domain.user.dto.request.ExtraInfoRequest; import com.ureca.uplait.domain.user.entity.User; @@ -73,9 +72,10 @@ void updateUserExtraInfo_success() { @DisplayName("전화번호 중복 - x") void isPhoneNumberDuplicated_returnsFalse() { String uniquePhoneNumber = "010-1234-5678"; + String currentPhoneNumber = null; given(userRepository.existsByPhoneNumber(uniquePhoneNumber)).willReturn(false); - boolean isDuplicated = userService.isPhoneNumberDuplicated(uniquePhoneNumber); + boolean isDuplicated = userService.isPhoneNumberDuplicated(uniquePhoneNumber, currentPhoneNumber); assertThat(isDuplicated).isFalse(); verify(userRepository, times(1)).existsByPhoneNumber(uniquePhoneNumber); @@ -85,21 +85,35 @@ void isPhoneNumberDuplicated_returnsFalse() { @DisplayName("전화번호 중복 - o") void isPhoneNumberDuplicated_returnsTrue() { String duplicatedPhoneNumber = "010-9876-5432"; + String currentPhoneNumber = null; given(userRepository.existsByPhoneNumber(duplicatedPhoneNumber)).willReturn(true); - boolean isDuplicated = userService.isPhoneNumberDuplicated(duplicatedPhoneNumber); + boolean isDuplicated = userService.isPhoneNumberDuplicated(duplicatedPhoneNumber, currentPhoneNumber); assertThat(isDuplicated).isTrue(); verify(userRepository, times(1)).existsByPhoneNumber(duplicatedPhoneNumber); } + @Test + @DisplayName("전화번호 중복 - 전화번호 변경이 없는 경우") + void isPhoneNumberDuplicated_samePhoneNumber() { + String uniquePhoneNumber = "010-1234-5678"; + String currentPhoneNumber = "010-1234-5678"; + + boolean isDuplicated = userService.isPhoneNumberDuplicated(uniquePhoneNumber, currentPhoneNumber); + + assertThat(isDuplicated).isFalse(); + verify(userRepository, never()).existsByPhoneNumber(uniquePhoneNumber); + } + @Test @DisplayName("이메일 중복 - x") void isEmailDuplicated_returnsFalse() { String uniqueEmail = "unique@example.com"; + String currentEmail = null; given(userRepository.existsByEmail(uniqueEmail)).willReturn(false); - boolean isDuplicated = userService.isEmailDuplicated(uniqueEmail); + boolean isDuplicated = userService.isEmailDuplicated(uniqueEmail, currentEmail); assertThat(isDuplicated).isFalse(); verify(userRepository, times(1)).existsByEmail(uniqueEmail); @@ -109,11 +123,24 @@ void isEmailDuplicated_returnsFalse() { @DisplayName("이메일 중복 - o") void isEmailDuplicated_returnsTrue() { String duplicatedEmail = "duplicated@example.com"; + String currentEmail = null; given(userRepository.existsByEmail(duplicatedEmail)).willReturn(true); - boolean isDuplicated = userService.isEmailDuplicated(duplicatedEmail); + boolean isDuplicated = userService.isEmailDuplicated(duplicatedEmail, currentEmail); assertThat(isDuplicated).isTrue(); verify(userRepository, times(1)).existsByEmail(duplicatedEmail); } + + @Test + @DisplayName("이메일 중복 - 이메일 변경이 없는 경우") + void isEmailDuplicated_sameEmail() { + String uniqueEmail = "unique@example.com"; + String currentEmail = "unique@example.com"; + + boolean isDuplicated = userService.isEmailDuplicated(uniqueEmail, currentEmail); + + assertThat(isDuplicated).isFalse(); + verify(userRepository, never()).existsByEmail(uniqueEmail); + } } \ No newline at end of file From e6a1b09fd7958b5faa13d589722742ebfeef53cf Mon Sep 17 00:00:00 2001 From: yoon_je Date: Fri, 20 Jun 2025 18:33:01 +0900 Subject: [PATCH 05/18] =?UTF-8?q?feat:=20=ED=83=9C=EA=B7=B8=20=EA=B8=B0?= =?UTF-8?q?=EB=B0=98=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84=EC=86=A1=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EC=9E=91=EC=84=B1=20=EC=A4=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 +++ .../domain/admin/service/AdminPlanService.java | 8 ++++++++ src/main/resources/application.yml | 12 ++++++++++++ 3 files changed, 23 insertions(+) diff --git a/build.gradle b/build.gradle index 35911fd..e5abb4b 100644 --- a/build.gradle +++ b/build.gradle @@ -69,6 +69,9 @@ dependencies { //validation implementation 'org.springframework.boot:spring-boot-starter-validation' + + //smtp + implementation 'org.springframework.boot:spring-boot-starter-mail' } tasks.named('test') { diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index 5204300..3d5f02c 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -47,6 +47,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import org.springframework.mail.SimpleMailMessage; +import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -82,6 +84,8 @@ public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); + + return new AdminPlanCreateResponse(savedPlan.getId()); } @@ -103,6 +107,8 @@ public AdminPlanCreateResponse createInternetPlan(AdminInternetPlanCreateRequest getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); + + return new AdminPlanCreateResponse(savedPlan.getId()); } @@ -124,6 +130,8 @@ public AdminPlanCreateResponse createIptvPlan(AdminIPTVPlanCreateRequest request getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); + + return new AdminPlanCreateResponse(savedPlan.getId()); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 1f9d617..07491c1 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,6 +23,18 @@ spring: liquibase: change-log: classpath:db/changelog/changelog-master.yaml enabled: true + mail: + host: smtp.gmail.com + port: 587 + username: ${MAIL_USERNAME} + password: ${MAIL_PASSWORD} + properties: + mail: + smtp: + auth: true + timeout: 5000 + starttls: + enable: true kakao: client-id: ${KAKAO_CLIENT_ID} From 7d734b58c398f2165cabc9b1408be2bd705e9f72 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Fri, 20 Jun 2025 19:12:13 +0900 Subject: [PATCH 06/18] =?UTF-8?q?test:=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/admin/service/AdminPlanServiceTest.java | 6 ++++++ .../uplait/domain/review/service/ReviewServiceTest.java | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/test/java/com/ureca/uplait/domain/admin/service/AdminPlanServiceTest.java b/src/test/java/com/ureca/uplait/domain/admin/service/AdminPlanServiceTest.java index 2e3adc0..a5d0a38 100644 --- a/src/test/java/com/ureca/uplait/domain/admin/service/AdminPlanServiceTest.java +++ b/src/test/java/com/ureca/uplait/domain/admin/service/AdminPlanServiceTest.java @@ -31,6 +31,8 @@ import com.ureca.uplait.domain.user.repository.TagRepository; import com.ureca.uplait.global.exception.GlobalException; import com.ureca.uplait.global.response.ResultCode; + +import java.util.HashSet; import java.util.List; import java.util.Optional; import org.junit.jupiter.api.DisplayName; @@ -231,6 +233,8 @@ void getAllMobilePlans_success() { .mediaBenefit(MediaBenefit.PREMIUM) .durationDiscountRate(10) .premierDiscountRate(5) + .planTags(new HashSet<>()) + .communityBenefitList(new HashSet<>()) .build(); MobilePlan plan2 = MobilePlan.builder() @@ -247,6 +251,8 @@ void getAllMobilePlans_success() { .mediaBenefit(MediaBenefit.PREMIUM) .durationDiscountRate(5) .premierDiscountRate(2) + .planTags(new HashSet<>()) + .communityBenefitList(new HashSet<>()) .build(); List mockResponses = List.of( diff --git a/src/test/java/com/ureca/uplait/domain/review/service/ReviewServiceTest.java b/src/test/java/com/ureca/uplait/domain/review/service/ReviewServiceTest.java index 8e16905..00d1305 100644 --- a/src/test/java/com/ureca/uplait/domain/review/service/ReviewServiceTest.java +++ b/src/test/java/com/ureca/uplait/domain/review/service/ReviewServiceTest.java @@ -1,5 +1,6 @@ package com.ureca.uplait.domain.review.service; +import com.ureca.uplait.domain.common.filter.BanWordFilter; import com.ureca.uplait.domain.plan.entity.MobilePlan; import com.ureca.uplait.domain.plan.repository.PlanRepository; import com.ureca.uplait.domain.review.dto.request.ReviewCreateRequest; @@ -35,6 +36,9 @@ class ReviewServiceTest { @Mock private PlanRepository planRepository; + @Mock + private BanWordFilter banWordFilter; + @InjectMocks private ReviewService reviewService; From 3df4137295bc7490201d765d685eb97ae8f125e9 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Sat, 21 Jun 2025 15:11:17 +0900 Subject: [PATCH 07/18] =?UTF-8?q?feat:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A0=84=EC=86=A1=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 7 + .../admin/controller/AdminPlanController.java | 7 + .../admin/service/AdminPlanService.java | 47 +++--- .../uplait/domain/batch/BatchConfig.java | 135 ++++++++++++++++++ .../com/ureca/uplait/domain/batch/Email.java | 11 ++ .../uplait/domain/batch/EmailBatchRunner.java | 36 +++++ .../domain/batch/EmailTemplateUtil.java | 82 +++++++++++ .../user/repository/UserRepository.java | 8 +- src/main/resources/application.yml | 5 + 9 files changed, 312 insertions(+), 26 deletions(-) create mode 100644 src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java create mode 100644 src/main/java/com/ureca/uplait/domain/batch/Email.java create mode 100644 src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java create mode 100644 src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java diff --git a/build.gradle b/build.gradle index e5abb4b..bcc56fb 100644 --- a/build.gradle +++ b/build.gradle @@ -72,8 +72,15 @@ dependencies { //smtp implementation 'org.springframework.boot:spring-boot-starter-mail' + + // spring batch + implementation 'org.springframework.boot:spring-boot-starter-batch' } tasks.named('test') { useJUnitPlatform() } + +tasks.withType(JavaCompile).configureEach { + options.compilerArgs << "-parameters" +} diff --git a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java index e8b91b0..0eb2197 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java +++ b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java @@ -28,6 +28,13 @@ public class AdminPlanController { private final AdminPlanService adminPlanService; + @Operation(summary = "TEST BATCH", description = "TEST BATCH") + @PostMapping("/batch") + public CommonResponse sendEmailBatch() { + adminPlanService.sendEmailBatch(); + return CommonResponse.success("응답 반환"); + } + @Operation(summary = "모바일 요금제 생성", description = "관리자가 새로운 모바일 요금제를 등록합니다.") @PostMapping("/mobile") public CommonResponse createMobilePlan( diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index 3d5f02c..7e6e732 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -1,33 +1,20 @@ package com.ureca.uplait.domain.admin.service; -import static com.ureca.uplait.domain.plan.util.DescriptionUtil.createDescription; - import com.ureca.uplait.domain.admin.api.FastAPIClient; -import com.ureca.uplait.domain.admin.dto.request.AdminIPTVPlanCreateRequest; -import com.ureca.uplait.domain.admin.dto.request.AdminIPTVPlanUpdateRequest; -import com.ureca.uplait.domain.admin.dto.request.AdminInternetPlanCreateRequest; -import com.ureca.uplait.domain.admin.dto.request.AdminInternetPlanUpdateRequest; -import com.ureca.uplait.domain.admin.dto.request.AdminMobileCreateRequest; -import com.ureca.uplait.domain.admin.dto.request.AdminMobilePlanUpdateRequest; +import com.ureca.uplait.domain.admin.dto.request.*; import com.ureca.uplait.domain.admin.dto.response.AdminPlanCreateResponse; import com.ureca.uplait.domain.admin.dto.response.AdminPlanDeleteResponse; import com.ureca.uplait.domain.admin.dto.response.AdminPlanDetailResponse; import com.ureca.uplait.domain.admin.dto.response.AdminUpdateAllVectorResponse; import com.ureca.uplait.domain.admin.repository.PlanVectorJdbcRepository; +import com.ureca.uplait.domain.batch.EmailBatchRunner; import com.ureca.uplait.domain.community.entity.CommunityBenefit; import com.ureca.uplait.domain.community.entity.CommunityBenefitPrice; import com.ureca.uplait.domain.community.entity.PlanCommunity; import com.ureca.uplait.domain.community.repository.CommunityBenefitPriceRepository; import com.ureca.uplait.domain.community.repository.CommunityBenefitRepository; import com.ureca.uplait.domain.community.repository.PlanCommunityRepository; -import com.ureca.uplait.domain.plan.dto.response.CommunityBenefitResponse; -import com.ureca.uplait.domain.plan.dto.response.IPTVPlanDetailResponse; -import com.ureca.uplait.domain.plan.dto.response.InternetPlanDetailResponse; -import com.ureca.uplait.domain.plan.dto.response.MobilePlanDetailResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanCreationInfoResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanDetailResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanResponseFactory; -import com.ureca.uplait.domain.plan.dto.response.TagResponse; +import com.ureca.uplait.domain.plan.dto.response.*; import com.ureca.uplait.domain.plan.entity.IPTVPlan; import com.ureca.uplait.domain.plan.entity.InternetPlan; import com.ureca.uplait.domain.plan.entity.MobilePlan; @@ -40,18 +27,20 @@ import com.ureca.uplait.domain.user.repository.TagRepository; import com.ureca.uplait.global.exception.GlobalException; import com.ureca.uplait.global.response.ResultCode; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.mail.SimpleMailMessage; -import org.springframework.mail.javamail.JavaMailSender; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import static com.ureca.uplait.domain.plan.util.DescriptionUtil.createDescription; + @Service @RequiredArgsConstructor @Transactional @@ -65,6 +54,17 @@ public class AdminPlanService { private final CommunityBenefitPriceRepository communityBenefitPriceRepository; private final PlanVectorJdbcRepository planVectorJdbcRepository; private final FastAPIClient fastAPIClient; + private final EmailBatchRunner emailBatchRunner; + + @Transactional + public void sendEmailBatch() { + Optional plan = planRepository.findById(1L); + List tagList = new ArrayList<>(List.of(1L, 2L, 3L)); + String tagIdStr = tagList.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + emailBatchRunner.runEmailBatchAsync(plan.get().getId(), tagIdStr); + } @Transactional public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request) { @@ -84,7 +84,8 @@ public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); - + // Batch 시행 +// emailBatchRunner.runEmailBatchAsync(savedPlan.getId()); return new AdminPlanCreateResponse(savedPlan.getId()); } diff --git a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java new file mode 100644 index 0000000..8578347 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java @@ -0,0 +1,135 @@ +package com.ureca.uplait.domain.batch; + +import com.ureca.uplait.domain.plan.entity.Plan; +import com.ureca.uplait.domain.plan.repository.PlanRepository; +import com.ureca.uplait.domain.user.entity.User; +import com.ureca.uplait.domain.user.repository.UserRepository; +import com.ureca.uplait.global.exception.GlobalException; +import jakarta.mail.internet.MimeMessage; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.Step; +import org.springframework.batch.core.configuration.annotation.StepScope; +import org.springframework.batch.core.job.builder.JobBuilder; +import org.springframework.batch.core.repository.JobRepository; +import org.springframework.batch.core.step.builder.StepBuilder; +import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.data.RepositoryItemReader; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.task.TaskExecutor; +import org.springframework.data.domain.Sort; +import org.springframework.mail.javamail.JavaMailSender; +import org.springframework.mail.javamail.MimeMessageHelper; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; +import org.springframework.transaction.PlatformTransactionManager; + +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +import static com.ureca.uplait.global.response.ResultCode.PLAN_NOT_FOUND; + +@Slf4j +@EnableAsync +@Configuration +public class BatchConfig { + + @Bean(name = "batchTaskExecutor") + public TaskExecutor batchTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(4); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("EmailBatch-"); + executor.initialize(); + return executor; + } + + @Bean + public Job emailSendJob(JobRepository jobRepository, Step sendStep) { + return new JobBuilder("emailSendJob", jobRepository) + .start(sendStep) + .build(); + } + + @Bean + public Step sendStep(JobRepository jobRepository, + PlatformTransactionManager transactionManager, + RepositoryItemReader userReader, + ItemProcessor emailProcessor, + ItemWriter emailWriter) { + + return new StepBuilder("sendStep", jobRepository) + .chunk(20, transactionManager) + .reader(userReader) + .processor(emailProcessor) + .writer(emailWriter) + .build(); + } + + @Bean + @StepScope + public RepositoryItemReader userReader(UserRepository userRepository) { + RepositoryItemReader reader = new RepositoryItemReader<>(); + reader.setRepository(userRepository); + reader.setMethodName("findAllByAdAgreeTrue"); + reader.setPageSize(100); + reader.setArguments(List.of()); + reader.setSort(Map.of("id", Sort.Direction.ASC)); + return reader; + } + + @Bean + @StepScope + public ItemProcessor emailProcessor( + JavaMailSender mailSender, + @Value("#{jobParameters['planId']}") String planIdStr, + @Value("#{jobParameters['tagIds']}") String tagIdsStr, + PlanRepository planRepository + ) { + return user -> { + // Plan 조회 + Long planId = Long.parseLong(planIdStr); + Plan plan = planRepository.findById(planId) + .orElseThrow(() -> new GlobalException(PLAN_NOT_FOUND)); + + // tagIds 파싱 및 조회 + List tagIds = Arrays.stream(tagIdsStr.split(",")) + .map(String::trim) + .filter(s -> !s.isEmpty()) + .map(Long::parseLong) + .collect(Collectors.toList()); +// List tags = tagRepository.findAllById(tagIds); + + // 이메일 생성 + Email email = EmailTemplateUtil.buildEmail(user, plan); + + MimeMessage message = mailSender.createMimeMessage(); + MimeMessageHelper helper = new MimeMessageHelper(message, false, "UTF-8"); + helper.setTo(user.getEmail()); + helper.setSubject(email.getTitle()); + helper.setText(email.getContent()); + + return message; + }; + } + + @Bean + public ItemWriter emailWriter(JavaMailSender mailSender) { + return messages -> { + for (MimeMessage msg : messages) { + try { + mailSender.send(msg); + log.info("[이메일 발송 성공] to={}", (Object) msg.getAllRecipients()); + } catch (Exception e) { + log.error("[이메일 발송 실패] to={}, reason={}", (Object) msg.getAllRecipients(), e.getMessage(), e); + } + } + }; + } +} diff --git a/src/main/java/com/ureca/uplait/domain/batch/Email.java b/src/main/java/com/ureca/uplait/domain/batch/Email.java new file mode 100644 index 0000000..a30d10d --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/batch/Email.java @@ -0,0 +1,11 @@ +package com.ureca.uplait.domain.batch; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class Email { + private String title; + private String content; +} diff --git a/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java b/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java new file mode 100644 index 0000000..050df12 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java @@ -0,0 +1,36 @@ +package com.ureca.uplait.domain.batch; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.batch.core.Job; +import org.springframework.batch.core.JobParametersBuilder; +import org.springframework.batch.core.launch.JobLauncher; +import org.springframework.core.task.TaskExecutor; +import org.springframework.scheduling.annotation.Async; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@RequiredArgsConstructor +public class EmailBatchRunner { + + private final JobLauncher jobLauncher; + private final Job emailSendJob; + + @Async + public void runEmailBatchAsync(Long planId, String tagIdsStr) { + try { + log.info("runEmailBatchRunner 실행"); + jobLauncher.run( + emailSendJob, + new JobParametersBuilder() + .addString("timestamp", String.valueOf(System.currentTimeMillis())) + .addString("planId", planId.toString()) + .addString("tagIds", tagIdsStr) + .toJobParameters() + ); + } catch (Exception e) { + log.error("이메일 배치 실행 실패: {}", e.getMessage(), e); + } + } +} diff --git a/src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java b/src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java new file mode 100644 index 0000000..802ec15 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java @@ -0,0 +1,82 @@ +package com.ureca.uplait.domain.batch; + +import com.ureca.uplait.domain.plan.entity.IPTVPlan; +import com.ureca.uplait.domain.plan.entity.InternetPlan; +import com.ureca.uplait.domain.plan.entity.MobilePlan; +import com.ureca.uplait.domain.plan.entity.Plan; +import com.ureca.uplait.domain.user.entity.User; +import com.ureca.uplait.global.exception.GlobalException; + +import static com.ureca.uplait.global.response.ResultCode.INVALID_PLAN; + +public class EmailTemplateUtil { + + public static Email buildEmail(User user, Plan plan) { + String title = String.format("[Uplait] 신규 요금제 '%s' 출시! 특별 혜택 확인하세요!", plan.getPlanName()); + String content = String.format( + "안녕하세요 %s님.\n\n" + + "Uplait에서 %s님이 관심있어하시는 주제와 관련된 새로운 요금제 '%s'가 출시되었습니다!\n" + + "요금: %,d원\n" + + buildDetail(plan) + + "아래 링크에서 자세한 혜택을 확인해보세요.\n\n" + + "자세히 보기: https://uplait.site/%s/plan/%s" + + "\n감사합니다,\nUplait 드림", + user.getName() != null ? user.getName() : "고객", + user.getName() != null ? user.getName() : "고객", + plan.getPlanName(), + plan.getPlanPrice(), + getType(plan), + plan.getId() + ); + return new Email(title, content); + } + + private static String buildDetail(Plan plan) { + if(plan instanceof MobilePlan mp) { + return buildMobile(mp); + } else if(plan instanceof InternetPlan ip) { + return buildInternet(ip); + } else if(plan instanceof IPTVPlan ip) { + return buildIptv(ip); + } else { + throw new GlobalException(INVALID_PLAN); + } + } + + private static String getType(Plan plan) { + if(plan instanceof MobilePlan) { + return "mobile"; + } else if(plan instanceof InternetPlan) { + return "internet"; + } else if(plan instanceof IPTVPlan) { + return "iptv"; + } else { + throw new GlobalException(INVALID_PLAN); + } + } + + private static String buildMobile(MobilePlan mp) { + return String.format( + "데이터: %s\n" + + "음성통화: %s\n" + + "문자: %s\n\n", + mp.getData(), + mp.getVoiceCall(), + mp.getMessage() + ); + } + + private static String buildInternet(InternetPlan ip) { + return String.format( + "인터넷 속도: %s\n", + ip.getVelocity() + ); + } + + private static String buildIptv(IPTVPlan ip) { + return String.format( + "채널 수: %s\n", + ip.getChannel() + ); + } +} diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java index e3f89ae..a96ece9 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java @@ -1,13 +1,15 @@ package com.ureca.uplait.domain.user.repository; -import java.util.Optional; - +import com.ureca.uplait.domain.user.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; -import com.ureca.uplait.domain.user.entity.User; +import java.util.Optional; public interface UserRepository extends JpaRepository { Optional findByKakaoId(String kakaoId); boolean existsByPhoneNumber(String phoneNumber); boolean existsByEmail(String email); + Page findAllByAdAgreeTrue(Pageable pageable); } diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 07491c1..3a10a53 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,6 +23,11 @@ spring: liquibase: change-log: classpath:db/changelog/changelog-master.yaml enabled: true + batch: + job: + enabled: false + jdbc: + initialize-schema: always mail: host: smtp.gmail.com port: 587 From 6a4c81dfc21a9a01b8d8ac06b959f250c83f2e63 Mon Sep 17 00:00:00 2001 From: etoile0626 Date: Sat, 21 Jun 2025 18:07:48 +0900 Subject: [PATCH 08/18] =?UTF-8?q?feat:=20native=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EA=B8=B0=EB=B0=98=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EC=A0=84?= =?UTF-8?q?=EC=86=A1=20=EB=8C=80=EC=83=81=EC=9E=90=20=EC=84=A0=EC=A0=95=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/batch/BatchConfig.java | 9 ++- .../user/repository/UserRepository.java | 67 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java index 8578347..81cc64a 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java +++ b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java @@ -74,12 +74,15 @@ public Step sendStep(JobRepository jobRepository, @Bean @StepScope - public RepositoryItemReader userReader(UserRepository userRepository) { + public RepositoryItemReader userReader( + UserRepository userRepository, + @Value("#{jobParameters['planId']}") Long planId // 배치 실행 시 외부에서 주입 + ) { RepositoryItemReader reader = new RepositoryItemReader<>(); reader.setRepository(userRepository); - reader.setMethodName("findAllByAdAgreeTrue"); + reader.setMethodName("findUsersWithMatchingTopTagsByPlanId"); reader.setPageSize(100); - reader.setArguments(List.of()); + reader.setArguments(List.of(planId)); // <- 여기 중요!! reader.setSort(Map.of("id", Sort.Direction.ASC)); return reader; } diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java index a96ece9..07bf1f9 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java @@ -4,7 +4,10 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import java.util.List; import java.util.Optional; public interface UserRepository extends JpaRepository { @@ -12,4 +15,68 @@ public interface UserRepository extends JpaRepository { boolean existsByPhoneNumber(String phoneNumber); boolean existsByEmail(String email); Page findAllByAdAgreeTrue(Pageable pageable); + + @Query( + value = """ + WITH target_plan_tags AS ( + SELECT tag_id + FROM plan_tag + WHERE plan_id = :planId + ), + user_top_tags AS ( + SELECT + user_id, + tag_id, + tag_count, + RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank + FROM user_tag + ), + user_top2_tags AS ( + SELECT * + FROM user_top_tags + WHERE rank <= 2 + ), + matched_users AS ( + SELECT DISTINCT ut.user_id + FROM user_top2_tags ut + JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id + ) + SELECT u.* + FROM users u + JOIN matched_users mu ON u.id = mu.user_id + WHERE u.ad_agree = true + """, + countQuery = """ + WITH target_plan_tags AS ( + SELECT tag_id + FROM plan_tag + WHERE plan_id = :planId + ), + user_top_tags AS ( + SELECT + user_id, + tag_id, + tag_count, + RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank + FROM user_tag + ), + user_top2_tags AS ( + SELECT * + FROM user_top_tags + WHERE rank <= 2 + ), + matched_users AS ( + SELECT DISTINCT ut.user_id + FROM user_top2_tags ut + JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id + ) + SELECT COUNT(*) + FROM users u + JOIN matched_users mu ON u.id = mu.user_id + WHERE u.ad_agree = true + """, + nativeQuery = true + ) + Page findUsersWithMatchingTopTagsByPlanId(@Param("planId") Long planId, Pageable pageable); + } From 5ef42f53a36f5272488e160757ab2748d97ea726 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang <149645095+Yyang-YE@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:25:52 +0900 Subject: [PATCH 09/18] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b8fc892..11c164a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI 실행 on: pull_request: - branches: [ "develop", "main" ] + branches: [ "develop", "main", "feat/tag-email" ] permissions: contents: read From 2317a449a42af96a1cef96836ad53fcb5c1a6048 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Sat, 21 Jun 2025 19:27:14 +0900 Subject: [PATCH 10/18] =?UTF-8?q?feat:=20=EC=9A=94=EA=B8=88=EC=A0=9C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20=EC=8B=9C=20batch=20=EC=8B=A4=ED=96=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminPlanService.java | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index 7e6e732..010f369 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -56,6 +56,7 @@ public class AdminPlanService { private final FastAPIClient fastAPIClient; private final EmailBatchRunner emailBatchRunner; + // TODO: 나중에 지우기 @Transactional public void sendEmailBatch() { Optional plan = planRepository.findById(1L); @@ -84,8 +85,11 @@ public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); - // Batch 시행 -// emailBatchRunner.runEmailBatchAsync(savedPlan.getId()); + // Batch 실행 + String tagIdStr = tagList.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + emailBatchRunner.runEmailBatchAsync(plan.getId(), tagIdStr); return new AdminPlanCreateResponse(savedPlan.getId()); } @@ -108,7 +112,11 @@ public AdminPlanCreateResponse createInternetPlan(AdminInternetPlanCreateRequest getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); - + // Batch 실행 + String tagIdStr = tagList.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + emailBatchRunner.runEmailBatchAsync(plan.getId(), tagIdStr); return new AdminPlanCreateResponse(savedPlan.getId()); } @@ -131,7 +139,11 @@ public AdminPlanCreateResponse createIptvPlan(AdminIPTVPlanCreateRequest request getPricesGroupedByBenefit(communityBenefitList)); fastAPIClient.saveVector(savedPlan, description); - + // Batch 실행 + String tagIdStr = tagList.stream() + .map(String::valueOf) + .collect(Collectors.joining(",")); + emailBatchRunner.runEmailBatchAsync(plan.getId(), tagIdStr); return new AdminPlanCreateResponse(savedPlan.getId()); } From 4dd9b7ca7792e9b5629beea6992dac35f34c4b94 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang <149645095+Yyang-YE@users.noreply.github.com> Date: Sat, 21 Jun 2025 19:30:12 +0900 Subject: [PATCH 11/18] Update ci.yml --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 11c164a..b8fc892 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI 실행 on: pull_request: - branches: [ "develop", "main", "feat/tag-email" ] + branches: [ "develop", "main" ] permissions: contents: read From 776fc8f50f9c1a7ca6c9849ce77f0e8624620a20 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Sat, 21 Jun 2025 20:11:11 +0900 Subject: [PATCH 12/18] =?UTF-8?q?feat:=20TMPUser=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminPlanController.java | 7 +- .../admin/service/AdminPlanService.java | 68 +++++++++++++++++-- .../uplait/domain/batch/EmailBatchRunner.java | 1 - 3 files changed, 69 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java index 0eb2197..960e60b 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java +++ b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java @@ -31,7 +31,12 @@ public class AdminPlanController { @Operation(summary = "TEST BATCH", description = "TEST BATCH") @PostMapping("/batch") public CommonResponse sendEmailBatch() { - adminPlanService.sendEmailBatch(); + // 이메일 발송 테스트용 +// adminPlanService.sendEmailBatch(); + + // User TMP 저장용 + adminPlanService.saveUserTemp(); + return CommonResponse.success("응답 반환"); } diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index 010f369..52820c5 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -23,8 +23,14 @@ import com.ureca.uplait.domain.user.entity.PlanTag; import com.ureca.uplait.domain.user.entity.Tag; import com.ureca.uplait.domain.user.entity.User; +import com.ureca.uplait.domain.user.entity.UserTag; +import com.ureca.uplait.domain.user.enums.Gender; +import com.ureca.uplait.domain.user.enums.Role; +import com.ureca.uplait.domain.user.enums.Status; import com.ureca.uplait.domain.user.repository.PlanTagRepository; import com.ureca.uplait.domain.user.repository.TagRepository; +import com.ureca.uplait.domain.user.repository.UserRepository; +import com.ureca.uplait.domain.user.repository.UserTagRepository; import com.ureca.uplait.global.exception.GlobalException; import com.ureca.uplait.global.response.ResultCode; import lombok.RequiredArgsConstructor; @@ -33,10 +39,7 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Optional; +import java.util.*; import java.util.stream.Collectors; import static com.ureca.uplait.domain.plan.util.DescriptionUtil.createDescription; @@ -56,7 +59,10 @@ public class AdminPlanService { private final FastAPIClient fastAPIClient; private final EmailBatchRunner emailBatchRunner; - // TODO: 나중에 지우기 + // TODO: 여기부터 지우기 + private final UserRepository userRepository; + private final UserTagRepository userTagRepository; + @Transactional public void sendEmailBatch() { Optional plan = planRepository.findById(1L); @@ -67,6 +73,58 @@ public void sendEmailBatch() { emailBatchRunner.runEmailBatchAsync(plan.get().getId(), tagIdStr); } + @Transactional + public void saveUserTemp() { + Random random = new Random(); + List allTags = tagRepository.findAll(); // 또는 필요한 범위만 조회 + boolean adAgree = true; + for (int i = 0; i < 2000; i++) { + // 1. 순차번호로 이름 생성 (예: "user1", "user2", ...) + long userCount = userRepository.count(); // 기존 유저 수 + String userName = "user" + (userCount + 1); + + // 유저 저장 + User user = User.builder() + .name(userName) + .kakaoId("1234") + .role(Role.USER) + .email("abc123@gmail.com") + .phoneNumber("010-1234-5678") + .gender(Gender.FEMALE) + .status(Status.ACTIVE) + .age(30) + .adAgree(adAgree) + .build(); + User savedUser = userRepository.save(user); + + // 2. 랜덤 Tag 번호(1~14), 랜덤 count(1~100) + Set usedTagIds = new HashSet<>(); + + int tagsToAssign = 10; + while (usedTagIds.size() < tagsToAssign) { + int idx = random.nextInt(allTags.size()); + Tag tag = allTags.get(idx); + + if (usedTagIds.contains(tag.getId())) { + continue; // 중복이면 건너뜀 + } + usedTagIds.add(tag.getId()); + + int count = random.nextInt(100) + 1; // 1~100 + + UserTag userTag = UserTag.builder() + .user(savedUser) + .tag(tag) // Tag 엔티티 직접 주입 + .tagCount(count) + .build(); + + userTagRepository.save(userTag); + } + adAgree = !adAgree; + } + } + // TODO: 여기까지 지우기 + @Transactional public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request) { validateDuplicatePlanName(request.getPlanName()); diff --git a/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java b/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java index 050df12..d9e7329 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java +++ b/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java @@ -5,7 +5,6 @@ import org.springframework.batch.core.Job; import org.springframework.batch.core.JobParametersBuilder; import org.springframework.batch.core.launch.JobLauncher; -import org.springframework.core.task.TaskExecutor; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; From f1992f7d7bc7cdbeb206283ce7ff4559a8785dda Mon Sep 17 00:00:00 2001 From: etoile0626 Date: Sat, 21 Jun 2025 21:06:16 +0900 Subject: [PATCH 13/18] =?UTF-8?q?feat:=20native=20=EC=BF=BC=EB=A6=AC=20JDB?= =?UTF-8?q?C=20=ED=83=AC=ED=94=8C=EB=A6=BF=EC=9C=BC=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/batch/BatchConfig.java | 16 +-- .../domain/batch/JdbcPagingUserReader.java | 36 ++++++ .../user/repository/UserJdbcRepository.java | 13 ++ .../repository/UserJdbcRepositoryImpl.java | 121 ++++++++++++++++++ .../user/repository/UserRepository.java | 66 +--------- 5 files changed, 177 insertions(+), 75 deletions(-) create mode 100644 src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java create mode 100644 src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java create mode 100644 src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java diff --git a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java index 81cc64a..07f1708 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java +++ b/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java @@ -14,6 +14,7 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.item.ItemProcessor; +import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; import org.springframework.batch.item.data.RepositoryItemReader; import org.springframework.beans.factory.annotation.Value; @@ -60,7 +61,7 @@ public Job emailSendJob(JobRepository jobRepository, Step sendStep) { @Bean public Step sendStep(JobRepository jobRepository, PlatformTransactionManager transactionManager, - RepositoryItemReader userReader, + ItemReader userReader, ItemProcessor emailProcessor, ItemWriter emailWriter) { @@ -74,17 +75,12 @@ public Step sendStep(JobRepository jobRepository, @Bean @StepScope - public RepositoryItemReader userReader( + public ItemReader userReader( UserRepository userRepository, - @Value("#{jobParameters['planId']}") Long planId // 배치 실행 시 외부에서 주입 + @Value("#{jobParameters['planId']}") Long planId ) { - RepositoryItemReader reader = new RepositoryItemReader<>(); - reader.setRepository(userRepository); - reader.setMethodName("findUsersWithMatchingTopTagsByPlanId"); - reader.setPageSize(100); - reader.setArguments(List.of(planId)); // <- 여기 중요!! - reader.setSort(Map.of("id", Sort.Direction.ASC)); - return reader; + int pageSize = 100; // 원하는 페이지 사이즈로 설정 + return new JdbcPagingUserReader(userRepository, planId, pageSize); } @Bean diff --git a/src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java b/src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java new file mode 100644 index 0000000..2282c40 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java @@ -0,0 +1,36 @@ +package com.ureca.uplait.domain.batch; + +import com.ureca.uplait.domain.user.entity.User; +import com.ureca.uplait.domain.user.repository.UserJdbcRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.batch.item.ItemReader; +import org.springframework.batch.item.support.IteratorItemReader; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Component; + +import java.util.Iterator; +import java.util.List; + +@RequiredArgsConstructor +public class JdbcPagingUserReader implements ItemReader { + + private final UserJdbcRepository userJdbcRepository; + private final Long planId; + private final int pageSize; + + private int currentPage = 0; + private Iterator currentIterator; + + @Override + public User read() { + if (currentIterator == null || !currentIterator.hasNext()) { + Pageable pageable = PageRequest.of(currentPage++, pageSize); + List users = userJdbcRepository.findUsersWithMatchingTopTagsByPlanId(planId, pageable); + if (users.isEmpty()) return null; // 배치 종료 조건 + currentIterator = users.iterator(); + } + + return currentIterator.next(); + } +} diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java new file mode 100644 index 0000000..0fa8387 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java @@ -0,0 +1,13 @@ +package com.ureca.uplait.domain.user.repository; + +import com.ureca.uplait.domain.user.entity.User; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface UserJdbcRepository { + List findUsersWithMatchingTopTagsByPlanId(Long planId, Pageable pageable); + long countUsersWithMatchingTopTagsByPlanId(Long planId); +} diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java new file mode 100644 index 0000000..9b1c391 --- /dev/null +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java @@ -0,0 +1,121 @@ +package com.ureca.uplait.domain.user.repository; + +import com.ureca.uplait.domain.user.entity.User; +import com.ureca.uplait.domain.user.enums.Gender; +import com.ureca.uplait.domain.user.enums.Role; +import com.ureca.uplait.domain.user.enums.Status; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Repository; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class UserJdbcRepositoryImpl implements UserJdbcRepository { + + private final JdbcTemplate jdbcTemplate; + + @Override + public List findUsersWithMatchingTopTagsByPlanId(Long planId, Pageable pageable) { + String sql = """ + WITH target_plan_tags AS ( + SELECT tag_id + FROM plan_tag + WHERE plan_id = ? + ), + user_top_tags AS ( + SELECT + user_id, + tag_id, + tag_count, + RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank + FROM user_tag + ), + user_top2_tags AS ( + SELECT * + FROM user_top_tags + WHERE rank <= 2 + ), + matched_users AS ( + SELECT DISTINCT ut.user_id + FROM user_top2_tags ut + JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id + ) + SELECT * + FROM users u + JOIN matched_users mu ON u.id = mu.user_id + WHERE u.ad_agree = true + ORDER BY u.id + LIMIT ? OFFSET ? + """; + + return jdbcTemplate.query( + sql, + new UserRowMapper(), + planId, + pageable.getPageSize(), + pageable.getOffset() + ); + } + + @Override + public long countUsersWithMatchingTopTagsByPlanId(Long planId) { + String countSql = """ + WITH target_plan_tags AS ( + SELECT tag_id + FROM plan_tag + WHERE plan_id = ? + ), + user_top_tags AS ( + SELECT + user_id, + tag_id, + tag_count, + RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank + FROM user_tag + ), + user_top2_tags AS ( + SELECT * + FROM user_top_tags + WHERE rank <= 2 + ), + matched_users AS ( + SELECT DISTINCT ut.user_id + FROM user_top2_tags ut + JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id + ) + SELECT COUNT(*) + FROM users u + JOIN matched_users mu ON u.id = mu.user_id + WHERE u.ad_agree = true + """; + + return jdbcTemplate.queryForObject(countSql, Long.class, planId); + } + + + private static class UserRowMapper implements RowMapper { + @Override + public User mapRow(ResultSet rs, int rowNum) throws SQLException { + return User.builder() + .name(rs.getString("name")) + .kakaoId(rs.getString("kakao_id")) + .phoneNumber(rs.getString("phone_number")) + .email(rs.getString("email")) + .age(rs.getInt("age")) + .gender(Gender.valueOf(rs.getString("gender"))) + .role(Role.valueOf(rs.getString("role"))) + .status(Status.valueOf(rs.getString("status"))) + .adAgree(rs.getBoolean("ad_agree")) + .build(); + } + } +} diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java index 07bf1f9..53f6df1 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserRepository.java @@ -10,73 +10,9 @@ import java.util.List; import java.util.Optional; -public interface UserRepository extends JpaRepository { +public interface UserRepository extends JpaRepository, UserJdbcRepository{ Optional findByKakaoId(String kakaoId); boolean existsByPhoneNumber(String phoneNumber); boolean existsByEmail(String email); Page findAllByAdAgreeTrue(Pageable pageable); - - @Query( - value = """ - WITH target_plan_tags AS ( - SELECT tag_id - FROM plan_tag - WHERE plan_id = :planId - ), - user_top_tags AS ( - SELECT - user_id, - tag_id, - tag_count, - RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank - FROM user_tag - ), - user_top2_tags AS ( - SELECT * - FROM user_top_tags - WHERE rank <= 2 - ), - matched_users AS ( - SELECT DISTINCT ut.user_id - FROM user_top2_tags ut - JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id - ) - SELECT u.* - FROM users u - JOIN matched_users mu ON u.id = mu.user_id - WHERE u.ad_agree = true - """, - countQuery = """ - WITH target_plan_tags AS ( - SELECT tag_id - FROM plan_tag - WHERE plan_id = :planId - ), - user_top_tags AS ( - SELECT - user_id, - tag_id, - tag_count, - RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank - FROM user_tag - ), - user_top2_tags AS ( - SELECT * - FROM user_top_tags - WHERE rank <= 2 - ), - matched_users AS ( - SELECT DISTINCT ut.user_id - FROM user_top2_tags ut - JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id - ) - SELECT COUNT(*) - FROM users u - JOIN matched_users mu ON u.id = mu.user_id - WHERE u.ad_agree = true - """, - nativeQuery = true - ) - Page findUsersWithMatchingTopTagsByPlanId(@Param("planId") Long planId, Pageable pageable); - } From 1908814d0a61b288468824703e7560b462d41e3d Mon Sep 17 00:00:00 2001 From: etoile0626 Date: Sun, 22 Jun 2025 00:20:59 +0900 Subject: [PATCH 14/18] =?UTF-8?q?feat:=201=EC=B0=A8=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(sql=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EC=A0=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/admin/controller/AdminPlanController.java | 4 ++-- .../uplait/domain/user/repository/UserJdbcRepositoryImpl.java | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java index 960e60b..d67f934 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java +++ b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java @@ -32,10 +32,10 @@ public class AdminPlanController { @PostMapping("/batch") public CommonResponse sendEmailBatch() { // 이메일 발송 테스트용 -// adminPlanService.sendEmailBatch(); + adminPlanService.sendEmailBatch(); // User TMP 저장용 - adminPlanService.saveUserTemp(); +// adminPlanService.saveUserTemp(); return CommonResponse.success("응답 반환"); } diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java index 9b1c391..a4bd620 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java @@ -49,6 +49,7 @@ matched_users AS ( FROM user_top2_tags ut JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id ) + SELECT * FROM users u JOIN matched_users mu ON u.id = mu.user_id From 90e4044780bb8ecf8e2091afb91e3d5eef7a84a0 Mon Sep 17 00:00:00 2001 From: etoile0626 Date: Sun, 22 Jun 2025 00:22:15 +0900 Subject: [PATCH 15/18] =?UTF-8?q?feat:=201=EC=B0=A8=20=EC=9D=B4=EB=A9=94?= =?UTF-8?q?=EC=9D=BC=20=EC=A0=84=EC=86=A1=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84(sql=20=EC=84=B1=EB=8A=A5=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EC=A0=84)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/user/repository/UserJdbcRepositoryImpl.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java index a4bd620..9b1c391 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java @@ -49,7 +49,6 @@ matched_users AS ( FROM user_top2_tags ut JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id ) - SELECT * FROM users u JOIN matched_users mu ON u.id = mu.user_id From f83d2b0e288b23b6e66e222cfe32ab67a28dbe36 Mon Sep 17 00:00:00 2001 From: etoile0626 Date: Sun, 22 Jun 2025 01:22:37 +0900 Subject: [PATCH 16/18] =?UTF-8?q?feat:=20=EC=BF=BC=EB=A6=AC=EC=B5=9C?= =?UTF-8?q?=EC=A0=81=ED=99=94=20-=20index=20=EC=A0=81=EC=9A=A9=20(index=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20sql=20=ED=8C=8C=EC=9D=BC=20=EB=85=B8?= =?UTF-8?q?=EC=85=98=EC=97=90=20=EA=B8=B0=EC=9E=AC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../uplait/domain/user/repository/UserJdbcRepositoryImpl.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java index 9b1c391..39dece5 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java @@ -5,10 +5,7 @@ import com.ureca.uplait.domain.user.enums.Role; import com.ureca.uplait.domain.user.enums.Status; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.stereotype.Repository; From 73d3a650e452bf5f83aaff050e38105adeb3a856 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Sun, 22 Jun 2025 17:53:31 +0900 Subject: [PATCH 17/18] =?UTF-8?q?refactor:=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/controller/AdminPlanController.java | 12 --- .../admin/service/AdminPlanService.java | 76 +------------------ .../{ => email}/batch/EmailBatchRunner.java | 2 +- .../batch/JdbcPagingUserReader.java | 4 +- .../domain/{batch => email/entity}/Email.java | 2 +- .../util}/EmailTemplateUtil.java | 3 +- .../user/repository/UserJdbcRepository.java | 3 - .../repository/UserJdbcRepositoryImpl.java | 36 --------- .../batch => global/config}/BatchConfig.java | 16 ++-- 9 files changed, 14 insertions(+), 140 deletions(-) rename src/main/java/com/ureca/uplait/domain/{ => email}/batch/EmailBatchRunner.java (96%) rename src/main/java/com/ureca/uplait/domain/{ => email}/batch/JdbcPagingUserReader.java (87%) rename src/main/java/com/ureca/uplait/domain/{batch => email/entity}/Email.java (77%) rename src/main/java/com/ureca/uplait/domain/{batch => email/util}/EmailTemplateUtil.java (96%) rename src/main/java/com/ureca/uplait/{domain/batch => global/config}/BatchConfig.java (90%) diff --git a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java index d67f934..e8b91b0 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java +++ b/src/main/java/com/ureca/uplait/domain/admin/controller/AdminPlanController.java @@ -28,18 +28,6 @@ public class AdminPlanController { private final AdminPlanService adminPlanService; - @Operation(summary = "TEST BATCH", description = "TEST BATCH") - @PostMapping("/batch") - public CommonResponse sendEmailBatch() { - // 이메일 발송 테스트용 - adminPlanService.sendEmailBatch(); - - // User TMP 저장용 -// adminPlanService.saveUserTemp(); - - return CommonResponse.success("응답 반환"); - } - @Operation(summary = "모바일 요금제 생성", description = "관리자가 새로운 모바일 요금제를 등록합니다.") @PostMapping("/mobile") public CommonResponse createMobilePlan( diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index 52820c5..f8dc180 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -7,7 +7,7 @@ import com.ureca.uplait.domain.admin.dto.response.AdminPlanDetailResponse; import com.ureca.uplait.domain.admin.dto.response.AdminUpdateAllVectorResponse; import com.ureca.uplait.domain.admin.repository.PlanVectorJdbcRepository; -import com.ureca.uplait.domain.batch.EmailBatchRunner; +import com.ureca.uplait.domain.email.batch.EmailBatchRunner; import com.ureca.uplait.domain.community.entity.CommunityBenefit; import com.ureca.uplait.domain.community.entity.CommunityBenefitPrice; import com.ureca.uplait.domain.community.entity.PlanCommunity; @@ -23,14 +23,8 @@ import com.ureca.uplait.domain.user.entity.PlanTag; import com.ureca.uplait.domain.user.entity.Tag; import com.ureca.uplait.domain.user.entity.User; -import com.ureca.uplait.domain.user.entity.UserTag; -import com.ureca.uplait.domain.user.enums.Gender; -import com.ureca.uplait.domain.user.enums.Role; -import com.ureca.uplait.domain.user.enums.Status; import com.ureca.uplait.domain.user.repository.PlanTagRepository; import com.ureca.uplait.domain.user.repository.TagRepository; -import com.ureca.uplait.domain.user.repository.UserRepository; -import com.ureca.uplait.domain.user.repository.UserTagRepository; import com.ureca.uplait.global.exception.GlobalException; import com.ureca.uplait.global.response.ResultCode; import lombok.RequiredArgsConstructor; @@ -59,72 +53,6 @@ public class AdminPlanService { private final FastAPIClient fastAPIClient; private final EmailBatchRunner emailBatchRunner; - // TODO: 여기부터 지우기 - private final UserRepository userRepository; - private final UserTagRepository userTagRepository; - - @Transactional - public void sendEmailBatch() { - Optional plan = planRepository.findById(1L); - List tagList = new ArrayList<>(List.of(1L, 2L, 3L)); - String tagIdStr = tagList.stream() - .map(String::valueOf) - .collect(Collectors.joining(",")); - emailBatchRunner.runEmailBatchAsync(plan.get().getId(), tagIdStr); - } - - @Transactional - public void saveUserTemp() { - Random random = new Random(); - List allTags = tagRepository.findAll(); // 또는 필요한 범위만 조회 - boolean adAgree = true; - for (int i = 0; i < 2000; i++) { - // 1. 순차번호로 이름 생성 (예: "user1", "user2", ...) - long userCount = userRepository.count(); // 기존 유저 수 - String userName = "user" + (userCount + 1); - - // 유저 저장 - User user = User.builder() - .name(userName) - .kakaoId("1234") - .role(Role.USER) - .email("abc123@gmail.com") - .phoneNumber("010-1234-5678") - .gender(Gender.FEMALE) - .status(Status.ACTIVE) - .age(30) - .adAgree(adAgree) - .build(); - User savedUser = userRepository.save(user); - - // 2. 랜덤 Tag 번호(1~14), 랜덤 count(1~100) - Set usedTagIds = new HashSet<>(); - - int tagsToAssign = 10; - while (usedTagIds.size() < tagsToAssign) { - int idx = random.nextInt(allTags.size()); - Tag tag = allTags.get(idx); - - if (usedTagIds.contains(tag.getId())) { - continue; // 중복이면 건너뜀 - } - usedTagIds.add(tag.getId()); - - int count = random.nextInt(100) + 1; // 1~100 - - UserTag userTag = UserTag.builder() - .user(savedUser) - .tag(tag) // Tag 엔티티 직접 주입 - .tagCount(count) - .build(); - - userTagRepository.save(userTag); - } - adAgree = !adAgree; - } - } - // TODO: 여기까지 지우기 - @Transactional public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request) { validateDuplicatePlanName(request.getPlanName()); @@ -145,7 +73,7 @@ public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request // Batch 실행 String tagIdStr = tagList.stream() - .map(String::valueOf) + .map(t -> String.valueOf(t.getId())) .collect(Collectors.joining(",")); emailBatchRunner.runEmailBatchAsync(plan.getId(), tagIdStr); diff --git a/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java b/src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java similarity index 96% rename from src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java rename to src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java index d9e7329..32523af 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/EmailBatchRunner.java +++ b/src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java @@ -1,4 +1,4 @@ -package com.ureca.uplait.domain.batch; +package com.ureca.uplait.domain.email.batch; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; diff --git a/src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java b/src/main/java/com/ureca/uplait/domain/email/batch/JdbcPagingUserReader.java similarity index 87% rename from src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java rename to src/main/java/com/ureca/uplait/domain/email/batch/JdbcPagingUserReader.java index 2282c40..122d0cc 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/JdbcPagingUserReader.java +++ b/src/main/java/com/ureca/uplait/domain/email/batch/JdbcPagingUserReader.java @@ -1,13 +1,11 @@ -package com.ureca.uplait.domain.batch; +package com.ureca.uplait.domain.email.batch; import com.ureca.uplait.domain.user.entity.User; import com.ureca.uplait.domain.user.repository.UserJdbcRepository; import lombok.RequiredArgsConstructor; import org.springframework.batch.item.ItemReader; -import org.springframework.batch.item.support.IteratorItemReader; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Component; import java.util.Iterator; import java.util.List; diff --git a/src/main/java/com/ureca/uplait/domain/batch/Email.java b/src/main/java/com/ureca/uplait/domain/email/entity/Email.java similarity index 77% rename from src/main/java/com/ureca/uplait/domain/batch/Email.java rename to src/main/java/com/ureca/uplait/domain/email/entity/Email.java index a30d10d..81ba829 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/Email.java +++ b/src/main/java/com/ureca/uplait/domain/email/entity/Email.java @@ -1,4 +1,4 @@ -package com.ureca.uplait.domain.batch; +package com.ureca.uplait.domain.email.entity; import lombok.AllArgsConstructor; import lombok.Getter; diff --git a/src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java b/src/main/java/com/ureca/uplait/domain/email/util/EmailTemplateUtil.java similarity index 96% rename from src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java rename to src/main/java/com/ureca/uplait/domain/email/util/EmailTemplateUtil.java index 802ec15..37eca5b 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/EmailTemplateUtil.java +++ b/src/main/java/com/ureca/uplait/domain/email/util/EmailTemplateUtil.java @@ -1,5 +1,6 @@ -package com.ureca.uplait.domain.batch; +package com.ureca.uplait.domain.email.util; +import com.ureca.uplait.domain.email.entity.Email; import com.ureca.uplait.domain.plan.entity.IPTVPlan; import com.ureca.uplait.domain.plan.entity.InternetPlan; import com.ureca.uplait.domain.plan.entity.MobilePlan; diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java index 0fa8387..d98f1f4 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java @@ -1,13 +1,10 @@ package com.ureca.uplait.domain.user.repository; import com.ureca.uplait.domain.user.entity.User; -import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; -import org.springframework.data.repository.query.Param; import java.util.List; public interface UserJdbcRepository { List findUsersWithMatchingTopTagsByPlanId(Long planId, Pageable pageable); - long countUsersWithMatchingTopTagsByPlanId(Long planId); } diff --git a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java index 39dece5..a8ebf0b 100644 --- a/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java +++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java @@ -63,42 +63,6 @@ matched_users AS ( ); } - @Override - public long countUsersWithMatchingTopTagsByPlanId(Long planId) { - String countSql = """ - WITH target_plan_tags AS ( - SELECT tag_id - FROM plan_tag - WHERE plan_id = ? - ), - user_top_tags AS ( - SELECT - user_id, - tag_id, - tag_count, - RANK() OVER (PARTITION BY user_id ORDER BY tag_count DESC) AS rank - FROM user_tag - ), - user_top2_tags AS ( - SELECT * - FROM user_top_tags - WHERE rank <= 2 - ), - matched_users AS ( - SELECT DISTINCT ut.user_id - FROM user_top2_tags ut - JOIN target_plan_tags tp ON ut.tag_id = tp.tag_id - ) - SELECT COUNT(*) - FROM users u - JOIN matched_users mu ON u.id = mu.user_id - WHERE u.ad_agree = true - """; - - return jdbcTemplate.queryForObject(countSql, Long.class, planId); - } - - private static class UserRowMapper implements RowMapper { @Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { diff --git a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java b/src/main/java/com/ureca/uplait/global/config/BatchConfig.java similarity index 90% rename from src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java rename to src/main/java/com/ureca/uplait/global/config/BatchConfig.java index 07f1708..2c69863 100644 --- a/src/main/java/com/ureca/uplait/domain/batch/BatchConfig.java +++ b/src/main/java/com/ureca/uplait/global/config/BatchConfig.java @@ -1,5 +1,8 @@ -package com.ureca.uplait.domain.batch; +package com.ureca.uplait.global.config; +import com.ureca.uplait.domain.email.batch.JdbcPagingUserReader; +import com.ureca.uplait.domain.email.entity.Email; +import com.ureca.uplait.domain.email.util.EmailTemplateUtil; import com.ureca.uplait.domain.plan.entity.Plan; import com.ureca.uplait.domain.plan.repository.PlanRepository; import com.ureca.uplait.domain.user.entity.User; @@ -16,12 +19,10 @@ import org.springframework.batch.item.ItemProcessor; import org.springframework.batch.item.ItemReader; import org.springframework.batch.item.ItemWriter; -import org.springframework.batch.item.data.RepositoryItemReader; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.TaskExecutor; -import org.springframework.data.domain.Sort; import org.springframework.mail.javamail.JavaMailSender; import org.springframework.mail.javamail.MimeMessageHelper; import org.springframework.scheduling.annotation.EnableAsync; @@ -30,8 +31,6 @@ import java.util.Arrays; import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; import static com.ureca.uplait.global.response.ResultCode.PLAN_NOT_FOUND; @@ -66,7 +65,7 @@ public Step sendStep(JobRepository jobRepository, ItemWriter emailWriter) { return new StepBuilder("sendStep", jobRepository) - .chunk(20, transactionManager) + .chunk(100, transactionManager) .reader(userReader) .processor(emailProcessor) .writer(emailWriter) @@ -102,8 +101,7 @@ public ItemProcessor emailProcessor( .map(String::trim) .filter(s -> !s.isEmpty()) .map(Long::parseLong) - .collect(Collectors.toList()); -// List tags = tagRepository.findAllById(tagIds); + .toList(); // 이메일 생성 Email email = EmailTemplateUtil.buildEmail(user, plan); @@ -126,7 +124,7 @@ public ItemWriter emailWriter(JavaMailSender mailSender) { mailSender.send(msg); log.info("[이메일 발송 성공] to={}", (Object) msg.getAllRecipients()); } catch (Exception e) { - log.error("[이메일 발송 실패] to={}, reason={}", (Object) msg.getAllRecipients(), e.getMessage(), e); + log.error("[이메일 발송 실패] to={}, reason={}", msg.getAllRecipients(), e.getMessage(), e); } } }; From 34c299f3558464242f3dd8507b74d3039d6796c6 Mon Sep 17 00:00:00 2001 From: Yeoeun Yang Date: Sun, 22 Jun 2025 18:17:05 +0900 Subject: [PATCH 18/18] =?UTF-8?q?chore:=20DTO=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/service/AdminPlanService.java | 2 +- .../dto/response/IPTVPlanDetailResponse.java | 7 +++--- .../response/InternetPlanDetailResponse.java | 7 +++--- .../response/MobilePlanDetailResponse.java | 7 +++--- .../plan/dto/response/PlanDetailResponse.java | 8 +++++- .../dto/response/PlanResponseFactory.java | 10 +++++--- .../domain/plan/service/PlanService.java | 25 ++++++++++--------- 7 files changed, 39 insertions(+), 27 deletions(-) diff --git a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java index f8dc180..1d539d6 100644 --- a/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java +++ b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java @@ -238,7 +238,7 @@ public AdminPlanDetailResponse getPlanDetail(Long planId) { public PlanDetailResponse getTypedPlanDetail(String type, Long planId) { Plan plan = getPlan(planId); - return PlanResponseFactory.from(plan, false); + return PlanResponseFactory.from(plan, null,false); } @Transactional diff --git a/src/main/java/com/ureca/uplait/domain/plan/dto/response/IPTVPlanDetailResponse.java b/src/main/java/com/ureca/uplait/domain/plan/dto/response/IPTVPlanDetailResponse.java index a98b9f2..c33a485 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/dto/response/IPTVPlanDetailResponse.java +++ b/src/main/java/com/ureca/uplait/domain/plan/dto/response/IPTVPlanDetailResponse.java @@ -2,9 +2,10 @@ import com.ureca.uplait.domain.plan.entity.IPTVPlan; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; import lombok.Getter; +import java.util.List; + @Getter @Schema(description = "IPTV 요금제 상세") public class IPTVPlanDetailResponse extends PlanDetailResponse { @@ -22,8 +23,8 @@ public class IPTVPlanDetailResponse extends PlanDetailResponse { @Schema(description = "결합 혜택", example = "가족결합") private List communityBenefitList; - public IPTVPlanDetailResponse(IPTVPlan plan, boolean inUse) { - super(plan, inUse); + public IPTVPlanDetailResponse(IPTVPlan plan, List communityIdList, boolean inUse) { + super(plan, communityIdList, inUse); this.channel = plan.getChannel(); this.iptvDiscount = plan.getPlanPrice() * (100 - plan.getIptvDiscountRate()) / 100; } diff --git a/src/main/java/com/ureca/uplait/domain/plan/dto/response/InternetPlanDetailResponse.java b/src/main/java/com/ureca/uplait/domain/plan/dto/response/InternetPlanDetailResponse.java index 8545ad5..6cafbdb 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/dto/response/InternetPlanDetailResponse.java +++ b/src/main/java/com/ureca/uplait/domain/plan/dto/response/InternetPlanDetailResponse.java @@ -2,9 +2,10 @@ import com.ureca.uplait.domain.plan.entity.InternetPlan; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; import lombok.Getter; +import java.util.List; + @Getter @Schema(description = "인터넷 요금제 상세") public class InternetPlanDetailResponse extends PlanDetailResponse { @@ -21,8 +22,8 @@ public class InternetPlanDetailResponse extends PlanDetailResponse { @Schema(description = "결합 혜택", example = "가족결합") private List communityBenefitList; - public InternetPlanDetailResponse(InternetPlan plan, boolean inUse) { - super(plan, inUse); + public InternetPlanDetailResponse(InternetPlan plan, List communityIdList, boolean inUse) { + super(plan, communityIdList, inUse); this.velocity = plan.getVelocity(); this.internetDiscount = plan.getInternetDiscountRate(); } diff --git a/src/main/java/com/ureca/uplait/domain/plan/dto/response/MobilePlanDetailResponse.java b/src/main/java/com/ureca/uplait/domain/plan/dto/response/MobilePlanDetailResponse.java index 7fdc808..6d6efed 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/dto/response/MobilePlanDetailResponse.java +++ b/src/main/java/com/ureca/uplait/domain/plan/dto/response/MobilePlanDetailResponse.java @@ -3,9 +3,10 @@ import com.ureca.uplait.domain.plan.entity.MediaBenefit; import com.ureca.uplait.domain.plan.entity.MobilePlan; import io.swagger.v3.oas.annotations.media.Schema; -import java.util.List; import lombok.Getter; +import java.util.List; + @Getter @Schema(description = "모바일 요금제 상세") public class MobilePlanDetailResponse extends PlanDetailResponse { @@ -40,8 +41,8 @@ public class MobilePlanDetailResponse extends PlanDetailResponse { @Schema(description = "결합 혜택", example = "가족결합") private List communityBenefitList; - public MobilePlanDetailResponse(MobilePlan plan, boolean inUse) { - super(plan, inUse); + public MobilePlanDetailResponse(MobilePlan plan, List communityIdList, boolean inUse) { + super(plan, communityIdList, inUse); this.data = plan.getData(); this.sharedData = plan.getSharedData(); this.voiceCall = plan.getVoiceCall(); diff --git a/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanDetailResponse.java b/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanDetailResponse.java index 3ee0f0e..433a0b6 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanDetailResponse.java +++ b/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanDetailResponse.java @@ -5,6 +5,8 @@ import io.swagger.v3.oas.annotations.media.Schema; import lombok.Getter; +import java.util.List; + @Getter @JsonInclude(JsonInclude.Include.NON_NULL) @Schema(description = "요금제 상세 조회 결과") @@ -37,14 +39,18 @@ public abstract class PlanDetailResponse { @Schema(description = "플랜 설명", example = "너무 좋은 요금제") private String description; + @Schema(description = "요금제의 결합 상품", example = "1, 2, 3") + private List communityIdList; + - protected PlanDetailResponse(Plan plan, boolean inUse) { + protected PlanDetailResponse(Plan plan, List communityIdList, boolean inUse) { this.planId = plan.getId(); this.planName = plan.getPlanName(); this.planPrice = plan.getPlanPrice(); this.planBenefit = plan.getPlanBenefit(); this.availability = plan.getAvailability(); this.description = plan.getDescription(); + this.communityIdList = communityIdList; this.inUse = inUse; } diff --git a/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanResponseFactory.java b/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanResponseFactory.java index eab3449..73fd838 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanResponseFactory.java +++ b/src/main/java/com/ureca/uplait/domain/plan/dto/response/PlanResponseFactory.java @@ -6,17 +6,19 @@ import com.ureca.uplait.domain.plan.entity.Plan; import com.ureca.uplait.global.exception.GlobalException; +import java.util.List; + import static com.ureca.uplait.global.response.ResultCode.INVALID_PLAN; public class PlanResponseFactory { - public static PlanDetailResponse from(Plan plan, boolean inUse) { + public static PlanDetailResponse from(Plan plan, List communityIdList, boolean inUse) { if (plan instanceof IPTVPlan iptv) { - return new IPTVPlanDetailResponse(iptv, inUse); + return new IPTVPlanDetailResponse(iptv, communityIdList, inUse); } else if (plan instanceof InternetPlan internet) { - return new InternetPlanDetailResponse(internet, inUse); + return new InternetPlanDetailResponse(internet, communityIdList, inUse); } else if (plan instanceof MobilePlan mobile) { - return new MobilePlanDetailResponse(mobile, inUse); + return new MobilePlanDetailResponse(mobile, communityIdList, inUse); } else { throw new GlobalException(INVALID_PLAN); } diff --git a/src/main/java/com/ureca/uplait/domain/plan/service/PlanService.java b/src/main/java/com/ureca/uplait/domain/plan/service/PlanService.java index 5afd440..44d2596 100644 --- a/src/main/java/com/ureca/uplait/domain/plan/service/PlanService.java +++ b/src/main/java/com/ureca/uplait/domain/plan/service/PlanService.java @@ -1,14 +1,8 @@ package com.ureca.uplait.domain.plan.service; -import static com.ureca.uplait.global.response.ResultCode.INVALID_INPUT; -import static com.ureca.uplait.global.response.ResultCode.PLAN_NOT_FOUND; - +import com.ureca.uplait.domain.community.repository.PlanCommunityRepository; import com.ureca.uplait.domain.contract.repository.ContractRepository; -import com.ureca.uplait.domain.plan.dto.response.PlanCompareFactory; -import com.ureca.uplait.domain.plan.dto.response.PlanCompareResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanDetailResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanListResponse; -import com.ureca.uplait.domain.plan.dto.response.PlanResponseFactory; +import com.ureca.uplait.domain.plan.dto.response.*; import com.ureca.uplait.domain.plan.entity.Plan; import com.ureca.uplait.domain.plan.repository.IPTVPlanRepository; import com.ureca.uplait.domain.plan.repository.InternetPlanRepository; @@ -16,13 +10,17 @@ import com.ureca.uplait.domain.plan.repository.PlanRepository; import com.ureca.uplait.domain.user.entity.User; import com.ureca.uplait.global.exception.GlobalException; -import java.util.Collections; -import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +import static com.ureca.uplait.global.response.ResultCode.INVALID_INPUT; +import static com.ureca.uplait.global.response.ResultCode.PLAN_NOT_FOUND; + @Service @RequiredArgsConstructor public class PlanService { @@ -32,12 +30,15 @@ public class PlanService { private final MobilePlanRepository mobilePlanRepository; private final InternetPlanRepository internetPlanRepository; private final IPTVPlanRepository iptvPlanRepository; + private final PlanCommunityRepository planCommunityRepository; @Transactional(readOnly = true) public PlanDetailResponse getPlanDetail(User user, Long planId) { Plan plan = findPlan(planId); + List communityIdList = planCommunityRepository.findAllByPlan(plan).stream() + .map(pc -> pc.getCommunityBenefit().getId()).toList(); boolean inUse = contractRepository.existsByUserIdAndPlanId(user.getId(), planId); - return PlanResponseFactory.from(plan, inUse); + return PlanResponseFactory.from(plan, communityIdList, inUse); } private Plan findPlan(Long planId) {