diff --git a/README.md b/README.md
index a4cb146..619cae5 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 문제인식
@@ -158,7 +158,7 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요
- ERD

-
+
# 6. 기대 효과
diff --git a/build.gradle b/build.gradle
index 35911fd..bcc56fb 100644
--- a/build.gradle
+++ b/build.gradle
@@ -69,8 +69,18 @@ dependencies {
//validation
implementation 'org.springframework.boot:spring-boot-starter-validation'
+
+ //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/service/AdminPlanService.java b/src/main/java/com/ureca/uplait/domain/admin/service/AdminPlanService.java
index 5204300..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
@@ -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.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;
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,16 +27,17 @@
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.ureca.uplait.domain.plan.util.DescriptionUtil.createDescription;
+
@Service
@RequiredArgsConstructor
@Transactional
@@ -63,6 +51,7 @@ public class AdminPlanService {
private final CommunityBenefitPriceRepository communityBenefitPriceRepository;
private final PlanVectorJdbcRepository planVectorJdbcRepository;
private final FastAPIClient fastAPIClient;
+ private final EmailBatchRunner emailBatchRunner;
@Transactional
public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request) {
@@ -82,6 +71,12 @@ public AdminPlanCreateResponse createMobilePlan(AdminMobileCreateRequest request
getPricesGroupedByBenefit(communityBenefitList));
fastAPIClient.saveVector(savedPlan, description);
+ // Batch 실행
+ String tagIdStr = tagList.stream()
+ .map(t -> String.valueOf(t.getId()))
+ .collect(Collectors.joining(","));
+ emailBatchRunner.runEmailBatchAsync(plan.getId(), tagIdStr);
+
return new AdminPlanCreateResponse(savedPlan.getId());
}
@@ -103,6 +98,12 @@ 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());
}
@@ -124,6 +125,12 @@ 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());
}
@@ -231,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/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)) {
diff --git a/src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java b/src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java
new file mode 100644
index 0000000..32523af
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/email/batch/EmailBatchRunner.java
@@ -0,0 +1,35 @@
+package com.ureca.uplait.domain.email.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.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/email/batch/JdbcPagingUserReader.java b/src/main/java/com/ureca/uplait/domain/email/batch/JdbcPagingUserReader.java
new file mode 100644
index 0000000..122d0cc
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/email/batch/JdbcPagingUserReader.java
@@ -0,0 +1,34 @@
+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.data.domain.PageRequest;
+import org.springframework.data.domain.Pageable;
+
+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/email/entity/Email.java b/src/main/java/com/ureca/uplait/domain/email/entity/Email.java
new file mode 100644
index 0000000..81ba829
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/email/entity/Email.java
@@ -0,0 +1,11 @@
+package com.ureca.uplait.domain.email.entity;
+
+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/email/util/EmailTemplateUtil.java b/src/main/java/com/ureca/uplait/domain/email/util/EmailTemplateUtil.java
new file mode 100644
index 0000000..37eca5b
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/email/util/EmailTemplateUtil.java
@@ -0,0 +1,83 @@
+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;
+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/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) {
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/repository/UserJdbcRepository.java b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java
new file mode 100644
index 0000000..d98f1f4
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepository.java
@@ -0,0 +1,10 @@
+package com.ureca.uplait.domain.user.repository;
+
+import com.ureca.uplait.domain.user.entity.User;
+import org.springframework.data.domain.Pageable;
+
+import java.util.List;
+
+public interface UserJdbcRepository {
+ List findUsersWithMatchingTopTagsByPlanId(Long planId, Pageable pageable);
+}
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..a8ebf0b
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/domain/user/repository/UserJdbcRepositoryImpl.java
@@ -0,0 +1,82 @@
+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.Pageable;
+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()
+ );
+ }
+
+ 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 e3f89ae..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
@@ -1,13 +1,18 @@
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 org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
-import com.ureca.uplait.domain.user.entity.User;
+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);
}
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/main/java/com/ureca/uplait/global/config/BatchConfig.java b/src/main/java/com/ureca/uplait/global/config/BatchConfig.java
new file mode 100644
index 0000000..2c69863
--- /dev/null
+++ b/src/main/java/com/ureca/uplait/global/config/BatchConfig.java
@@ -0,0 +1,132 @@
+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;
+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.ItemReader;
+import org.springframework.batch.item.ItemWriter;
+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.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 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,
+ ItemReader userReader,
+ ItemProcessor emailProcessor,
+ ItemWriter emailWriter) {
+
+ return new StepBuilder("sendStep", jobRepository)
+ .chunk(100, transactionManager)
+ .reader(userReader)
+ .processor(emailProcessor)
+ .writer(emailWriter)
+ .build();
+ }
+
+ @Bean
+ @StepScope
+ public ItemReader userReader(
+ UserRepository userRepository,
+ @Value("#{jobParameters['planId']}") Long planId
+ ) {
+ int pageSize = 100; // 원하는 페이지 사이즈로 설정
+ return new JdbcPagingUserReader(userRepository, planId, pageSize);
+ }
+
+ @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)
+ .toList();
+
+ // 이메일 생성
+ 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={}", msg.getAllRecipients(), e.getMessage(), e);
+ }
+ }
+ };
+ }
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index 1f9d617..3a10a53 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -23,6 +23,23 @@ 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
+ username: ${MAIL_USERNAME}
+ password: ${MAIL_PASSWORD}
+ properties:
+ mail:
+ smtp:
+ auth: true
+ timeout: 5000
+ starttls:
+ enable: true
kakao:
client-id: ${KAKAO_CLIENT_ID}
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/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/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;
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