Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
dd13fb8
update: 로그아웃 서비스 메서드 수정
etoile0626 Jun 20, 2025
3aa3acb
Update README.md
djlim00 Jun 20, 2025
844bb52
Update README.md
djlim00 Jun 20, 2025
9c3df8b
fix: 전화번호, 이메일 중복검사 로직 수정, 그에 따른 테스트 코드 수정 및 추가
etoile0626 Jun 20, 2025
f2b09ad
Merge branch 'develop' into fix/UP-85-profile-edit-fix
etoile0626 Jun 20, 2025
4d9765d
Merge pull request #77 from U-plait/fix/UP-85-profile-edit-fix
etoile0626 Jun 20, 2025
e6a1b09
feat: 태그 기반 이메일 전송 기능 작성 중
etoile0626 Jun 20, 2025
7d734b5
test: 테스트 오류 수정
Yyang-YE Jun 20, 2025
3df4137
feat: 이메일 전송 구현
Yyang-YE Jun 21, 2025
6a4c81d
feat: native 쿼리 기반 이메일 전송 대상자 선정 로직 구현
etoile0626 Jun 21, 2025
5ef42f5
Update ci.yml
Yyang-YE Jun 21, 2025
2317a44
feat: 요금제 추가 시 batch 실행
Yyang-YE Jun 21, 2025
0b60ac6
Merge branch 'develop' of https://github.com/U-plait/u-plait-be into …
Yyang-YE Jun 21, 2025
4dd9b7c
Update ci.yml
Yyang-YE Jun 21, 2025
ac813b0
Merge branch 'develop' of https://github.com/U-plait/u-plait-be into …
Yyang-YE Jun 21, 2025
776fc8f
feat: TMPUser 저장 로직 추가
Yyang-YE Jun 21, 2025
f1992f7
feat: native 쿼리 JDBC 탬플릿으로 변경
etoile0626 Jun 21, 2025
1908814
feat: 1차 이메일 전송 기능 구현(sql 성능 개선 전)
etoile0626 Jun 21, 2025
90e4044
feat: 1차 이메일 전송 기능 구현(sql 성능 개선 전)
etoile0626 Jun 21, 2025
f83d2b0
feat: 쿼리최적화 - index 적용 (index 생성 sql 파일 노션에 기재)
etoile0626 Jun 21, 2025
73d3a65
refactor: 파일 구조 정리
Yyang-YE Jun 22, 2025
34c299f
chore: DTO 수정
Yyang-YE Jun 22, 2025
3e8e41a
Merge pull request #80 from U-plait/feat/tag-email
etoile0626 Jun 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
<br><br />
# 1. 프로젝트의 배경
### 1.1 문제인식
Expand Down Expand Up @@ -158,7 +158,7 @@ LLM 기반 챗봇과의 대화를 통해 사용자는 자신에게 알맞는 요

- ERD
<br>![Image](https://github.com/user-attachments/assets/271fbd65-50ba-4528-a96c-3ae0b16a7d34)

<br><br />

# 6. 기대 효과

Expand Down
10 changes: 10 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand All @@ -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) {
Expand All @@ -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());
}

Expand All @@ -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());
}

Expand All @@ -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());
}

Expand Down Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
Original file line number Diff line number Diff line change
@@ -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<User> {

private final UserJdbcRepository userJdbcRepository;
private final Long planId;
private final int pageSize;

private int currentPage = 0;
private Iterator<User> currentIterator;

@Override
public User read() {
if (currentIterator == null || !currentIterator.hasNext()) {
Pageable pageable = PageRequest.of(currentPage++, pageSize);
List<User> users = userJdbcRepository.findUsersWithMatchingTopTagsByPlanId(planId, pageable);
if (users.isEmpty()) return null; // 배치 종료 조건
currentIterator = users.iterator();
}

return currentIterator.next();
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/ureca/uplait/domain/email/entity/Email.java
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
@@ -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()
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -22,8 +23,8 @@ public class IPTVPlanDetailResponse extends PlanDetailResponse {
@Schema(description = "결합 혜택", example = "가족결합")
private List<CommunityBenefitResponse> communityBenefitList;

public IPTVPlanDetailResponse(IPTVPlan plan, boolean inUse) {
super(plan, inUse);
public IPTVPlanDetailResponse(IPTVPlan plan, List<Long> communityIdList, boolean inUse) {
super(plan, communityIdList, inUse);
this.channel = plan.getChannel();
this.iptvDiscount = plan.getPlanPrice() * (100 - plan.getIptvDiscountRate()) / 100;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -21,8 +22,8 @@ public class InternetPlanDetailResponse extends PlanDetailResponse {
@Schema(description = "결합 혜택", example = "가족결합")
private List<CommunityBenefitResponse> communityBenefitList;

public InternetPlanDetailResponse(InternetPlan plan, boolean inUse) {
super(plan, inUse);
public InternetPlanDetailResponse(InternetPlan plan, List<Long> communityIdList, boolean inUse) {
super(plan, communityIdList, inUse);
this.velocity = plan.getVelocity();
this.internetDiscount = plan.getInternetDiscountRate();
}
Expand Down
Loading
Loading