Skip to content

Commit e012287

Browse files
authored
Merge 7b7e9ca into f151ed1
2 parents f151ed1 + 7b7e9ca commit e012287

File tree

13 files changed

+233
-6
lines changed

13 files changed

+233
-6
lines changed

build.gradle

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ dependencies {
8383

8484
// circuit breaker dependencies
8585
implementation 'io.github.resilience4j:resilience4j-spring-boot3:2.2.0'
86+
87+
// mail
88+
implementation 'org.springframework.boot:spring-boot-starter-mail'
89+
8690
}
8791

8892
dependencyManagement {

src/main/java/sevenstar/marineleisure/favorite/repository/FavoriteRepository.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,12 @@ List<FavoriteItemVO> findFavoritesByMemberIdAndCursorId(
3636
Pageable pageable
3737
);
3838
boolean existsByMemberIdAndSpotId(Long memberId, Long spotId);
39+
40+
@Query(value = """
41+
SELECT m.email
42+
FROM FavoriteSpot fs
43+
JOIN Member m ON fs.memberId = m.id
44+
WHERE fs.spotId = :spotId
45+
""")
46+
List<String> findEmailByFavoriteBestSpot(Long spotId);
3947
}

src/main/java/sevenstar/marineleisure/global/api/scheduler/SchedulerService.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import sevenstar.marineleisure.global.api.kakao.service.PresetSchedulerService;
1313
import sevenstar.marineleisure.global.api.khoa.service.KhoaApiService;
1414
import sevenstar.marineleisure.global.api.openmeteo.dto.service.OpenMeteoService;
15+
import sevenstar.marineleisure.global.mail.MailService;
1516
import sevenstar.marineleisure.spot.repository.SpotViewQuartileRepository;
1617

1718
@Service
@@ -24,7 +25,7 @@ public class SchedulerService {
2425
private final PresetSchedulerService presetSchedulerService;
2526
private final SpotViewQuartileRepository spotViewQuartileRepository;
2627

27-
28+
private final MailService mailService;
2829
private final Executor taskExecutor;
2930

3031
/**
@@ -55,6 +56,12 @@ public void scheduler() {
5556
// 모든 병렬 작업이 완료될 때까지 기다림
5657
CompletableFuture.allOf(openMeteoFuture, presetSchedulerFuture, spotViewQuartileFuture).join();
5758

59+
try {
60+
mailService.sendMailToHaveFavoriteBestSpot(today);
61+
} catch (Exception e) {
62+
log.error("Error sending mail to users with favorite best spots", e);
63+
}
64+
5865
log.info("=== update data ===");
5966
}
6067
}

src/main/java/sevenstar/marineleisure/global/enums/ActivityCategory.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
package sevenstar.marineleisure.global.enums;
22

3+
import lombok.Getter;
34
import sevenstar.marineleisure.global.exception.CustomException;
45
import sevenstar.marineleisure.global.exception.enums.CommonErrorCode;
56

7+
@Getter
68
public enum ActivityCategory {
7-
FISHING,
8-
SURFING,
9-
SCUBA,
10-
MUDFLAT;
9+
FISHING("낚시"),
10+
SURFING("서핑"),
11+
SCUBA("스쿠버다이빙"),
12+
MUDFLAT("갯벌체험");
13+
14+
private String koreanName;
15+
16+
ActivityCategory(String koreanName) {
17+
this.koreanName = koreanName;
18+
}
1119

1220
public static ActivityCategory parse(String category) {
1321
try {
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
package sevenstar.marineleisure.global.mail;
2+
3+
import java.time.LocalDate;
4+
import java.util.ArrayList;
5+
import java.util.EnumMap;
6+
import java.util.HashMap;
7+
import java.util.HashSet;
8+
import java.util.List;
9+
import java.util.Map;
10+
import java.util.Set;
11+
12+
import org.springframework.mail.javamail.JavaMailSender;
13+
import org.springframework.mail.javamail.MimeMessageHelper;
14+
import org.springframework.stereotype.Service;
15+
16+
import jakarta.mail.internet.MimeMessage;
17+
import lombok.RequiredArgsConstructor;
18+
import lombok.extern.slf4j.Slf4j;
19+
import sevenstar.marineleisure.favorite.repository.FavoriteRepository;
20+
import sevenstar.marineleisure.global.enums.ActivityCategory;
21+
import sevenstar.marineleisure.global.enums.TotalIndex;
22+
import sevenstar.marineleisure.spot.dto.EmailContent;
23+
import sevenstar.marineleisure.spot.dto.detail.provider.ActivityProvider;
24+
25+
@Service
26+
@RequiredArgsConstructor
27+
@Slf4j
28+
public class MailService {
29+
private static final String MESSAGE_SUBJECT = "[MarineLeisure] 즐겨찾기한 스팟이 최상의 컨디션이에요!";
30+
31+
private final JavaMailSender javaMailSender;
32+
private final FavoriteRepository favoriteRepository;
33+
private final List<ActivityProvider> providers;
34+
35+
public void sendMail(String to, String subject, String htmlContent) {
36+
try {
37+
MimeMessage mimeMessage = javaMailSender.createMimeMessage();
38+
MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, "UTF-8");
39+
helper.setFrom("[email protected]");
40+
helper.setTo(to);
41+
helper.setSubject(subject);
42+
helper.setText(htmlContent, true); // true = HTML
43+
44+
javaMailSender.send(mimeMessage);
45+
} catch (Exception e) {
46+
log.error("메일 전송 실패", e);
47+
}
48+
}
49+
50+
51+
public void sendMailToHaveFavoriteBestSpot(LocalDate date) {
52+
TotalIndex totalIndex = TotalIndex.VERY_GOOD;
53+
List<EmailContent> emailContents = new ArrayList<>();
54+
for (ActivityProvider provider : providers) {
55+
emailContents.addAll(provider.findEmailContent(totalIndex, date));
56+
}
57+
Map<String, Map<ActivityCategory, Set<String>>> result = new HashMap<>();
58+
for (EmailContent emailContent : emailContents) {
59+
List<String> emails = favoriteRepository.findEmailByFavoriteBestSpot(emailContent.spotId());
60+
for (String email : emails) {
61+
if (result.containsKey(email)) {
62+
result.get(email).get(emailContent.category()).add(emailContent.spotName());
63+
} else {
64+
Map<ActivityCategory, Set<String>> map = new EnumMap<>(ActivityCategory.class);
65+
for (ActivityCategory value : ActivityCategory.values()) {
66+
map.put(value, new HashSet<>());
67+
}
68+
map.get(emailContent.category()).add(emailContent.spotName());
69+
result.put(email, map);
70+
}
71+
}
72+
}
73+
for (Map.Entry<String, Map<ActivityCategory, Set<String>>> entry : result.entrySet()) {
74+
sendMail(entry.getKey(), MESSAGE_SUBJECT, transformEmailContent(entry.getValue()));
75+
}
76+
}
77+
78+
// private String transformEmailContent(Map<ActivityCategory, Set<String>> map) {
79+
// StringBuilder sb = new StringBuilder();
80+
// sb.append("<div style='font-family: Arial, sans-serif; font-size: 14px;'>");
81+
// sb.append("<p>안녕하세요, <strong>MarineLeisure</strong>입니다 🌊</p>");
82+
// sb.append("<p>고객님이 즐겨찾기한 장소 중, 오늘 같은 날 <strong>최상의 컨디션</strong>을 보이는 스팟들을 추천드립니다.</p>");
83+
//
84+
// sb.append("<ul>");
85+
// for (ActivityCategory category : ActivityCategory.values()) {
86+
// Set<String> spots = map.getOrDefault(category, Set.of());
87+
// String spotList = spots.isEmpty() ? "없어요 😢" : String.join(", ", spots);
88+
// sb.append("<li><strong>")
89+
// .append(category.getKoreanName())
90+
// .append("</strong>에 좋은 스팟: ")
91+
// .append(spotList)
92+
// .append("</li>");
93+
// }
94+
// sb.append("</ul>");
95+
//
96+
// sb.append("<p>👉 <a href=\"https://marineleisure.com\" target=\"_blank\">MarineLeisure 앱에서 자세히 보기</a></p>");
97+
// sb.append("<p>안전하고 즐거운 하루 보내세요 😊<br>MarineLeisure 드림</p>");
98+
// sb.append("</div>");
99+
//
100+
// return sb.toString();
101+
// }
102+
103+
private String transformEmailContent(Map<ActivityCategory, Set<String>> map) {
104+
StringBuilder sb = new StringBuilder();
105+
106+
sb.append("<div style='font-family: \"Apple SD Gothic Neo\", sans-serif; background-color: #f4f4f4; padding: 20px;'>")
107+
.append("<div style='max-width: 600px; margin: auto; background-color: #ffffff; border-radius: 10px; padding: 30px; box-shadow: 0 0 10px rgba(0,0,0,0.05);'>")
108+
109+
.append("<h2 style='color: #0077b6; text-align: center;'>🌊 MarineLeisure 추천 스팟 알림</h2>")
110+
.append("<p style='font-size: 15px; color: #333;'>")
111+
.append("고객님이 즐겨찾기한 해양 활동 스팟 중, 오늘 같은 날 <strong style='color: #0077b6;'>최고의 컨디션</strong>을 보이는 장소를 추천드릴게요!")
112+
.append("</p>");
113+
114+
for (ActivityCategory category : ActivityCategory.values()) {
115+
Set<String> spots = map.getOrDefault(category, Set.of());
116+
if (!spots.isEmpty()) {
117+
sb.append("<div style='margin-top: 20px;'>")
118+
.append("<h3 style='color: #023e8a; font-size: 16px;'>")
119+
.append("✔️ ").append(category.getKoreanName()).append(" 추천 스팟")
120+
.append("</h3>")
121+
.append("<ul style='padding-left: 20px;'>");
122+
for (String spot : spots) {
123+
sb.append("<li>").append(spot).append("</li>");
124+
}
125+
sb.append("</ul></div>");
126+
}
127+
}
128+
129+
sb.append("<div style='text-align: center; margin-top: 30px;'>")
130+
.append("<a href='https://marineleisure.vercel.app' target='_blank' style='background-color: #00b4d8; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; font-weight: bold;'>MarineLeisure 앱에서 확인하기</a>")
131+
.append("</div>")
132+
133+
.append("<p style='margin-top: 30px; font-size: 14px; color: #555;'>")
134+
.append("안전하고 즐거운 하루 보내세요!<br><strong>MarineLeisure 드림</strong>")
135+
.append("</p>")
136+
137+
.append("</div></div>");
138+
139+
return sb.toString();
140+
}
141+
142+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
package sevenstar.marineleisure.spot.dto;
2+
3+
import sevenstar.marineleisure.global.enums.ActivityCategory;
4+
5+
public record EmailContent(
6+
Long spotId,
7+
String spotName,
8+
ActivityCategory category
9+
) {
10+
}

src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ActivityProvider.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import sevenstar.marineleisure.global.api.openmeteo.dto.item.UvIndexItem;
1919
import sevenstar.marineleisure.global.enums.ActivityCategory;
2020
import sevenstar.marineleisure.global.enums.FishingType;
21+
import sevenstar.marineleisure.global.enums.TotalIndex;
2122
import sevenstar.marineleisure.global.utils.GeoUtils;
2223
import sevenstar.marineleisure.spot.domain.OutdoorSpot;
24+
import sevenstar.marineleisure.spot.dto.EmailContent;
2325
import sevenstar.marineleisure.spot.repository.ActivityRepository;
2426
import sevenstar.marineleisure.spot.repository.OutdoorSpotRepository;
2527

@@ -43,6 +45,8 @@ public abstract class ActivityProvider {
4345

4446
public abstract void update(LocalDate startDate, LocalDate endDate);
4547

48+
public abstract List<EmailContent> findEmailContent(TotalIndex totalIndex, LocalDate forecastDate);
49+
4650
@Transactional
4751
protected OutdoorSpot createOutdoorSpot(KhoaItem item, FishingType fishingType) {
4852
return outdoorSpotRepository.findByLatitudeAndLongitudeAndCategory(item.getLatitude(), item.getLongitude(),

src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/FishingProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import sevenstar.marineleisure.global.enums.TotalIndex;
2424
import sevenstar.marineleisure.global.utils.DateUtils;
2525
import sevenstar.marineleisure.spot.domain.OutdoorSpot;
26+
import sevenstar.marineleisure.spot.dto.EmailContent;
2627
import sevenstar.marineleisure.spot.dto.projection.FishingReadProjection;
2728
import sevenstar.marineleisure.spot.mapper.SpotDetailMapper;
2829
import sevenstar.marineleisure.spot.repository.ActivityRepository;
@@ -94,6 +95,11 @@ public void update(LocalDate startDate, LocalDate endDate) {
9495
}
9596
}
9697

98+
@Override
99+
public List<EmailContent> findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) {
100+
return fishingRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate);
101+
}
102+
97103
private List<ActivitySpotDetail> transform(List<FishingReadProjection> fishingForecasts) {
98104
List<ActivitySpotDetail> details = new ArrayList<>();
99105
for (FishingReadProjection fishingForecast : fishingForecasts) {

src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/MudflatProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import sevenstar.marineleisure.global.enums.TotalIndex;
2020
import sevenstar.marineleisure.global.utils.DateUtils;
2121
import sevenstar.marineleisure.spot.domain.OutdoorSpot;
22+
import sevenstar.marineleisure.spot.dto.EmailContent;
2223
import sevenstar.marineleisure.spot.mapper.SpotDetailMapper;
2324
import sevenstar.marineleisure.spot.repository.ActivityRepository;
2425

@@ -73,6 +74,11 @@ public void update(LocalDate startDate, LocalDate endDate) {
7374
}
7475
}
7576

77+
@Override
78+
public List<EmailContent> findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) {
79+
return mudflatRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate);
80+
}
81+
7682
private List<ActivitySpotDetail> transform(List<Mudflat> mudflatForecasts) {
7783
List<ActivitySpotDetail> details = new ArrayList<>();
7884
for (Mudflat mudflatForecast : mudflatForecasts) {

src/main/java/sevenstar/marineleisure/spot/dto/detail/provider/ScubaProvider.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import sevenstar.marineleisure.global.enums.TotalIndex;
2222
import sevenstar.marineleisure.global.utils.DateUtils;
2323
import sevenstar.marineleisure.spot.domain.OutdoorSpot;
24+
import sevenstar.marineleisure.spot.dto.EmailContent;
2425
import sevenstar.marineleisure.spot.mapper.SpotDetailMapper;
2526
import sevenstar.marineleisure.spot.repository.ActivityRepository;
2627

@@ -76,6 +77,11 @@ public void update(LocalDate startDate, LocalDate endDate) {
7677
}
7778
}
7879

80+
@Override
81+
public List<EmailContent> findEmailContent(TotalIndex totalIndex, LocalDate forecastDate) {
82+
return scubaRepository.findEmailContentByTotalIndexAndForecastDate(totalIndex, forecastDate);
83+
}
84+
7985
private List<ActivitySpotDetail> transform(List<Scuba> scubaForecasts) {
8086
List<ActivitySpotDetail> details = new ArrayList<>();
8187
for (Scuba scubaForecast : scubaForecasts) {

0 commit comments

Comments
 (0)