Skip to content

Commit 690a40c

Browse files
Merge pull request #131 from prgrms-web-devcourse-final-project/feat/#126
[Concert,Auth] 공연 예매일정 알림 서비스 리팩토링, 메일 양식 개선, 인증 이메일 전반 양식 개선
2 parents 32c4a63 + 338d4c8 commit 690a40c

7 files changed

Lines changed: 530 additions & 112 deletions

File tree

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/ConcertLikeRepository.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ public interface ConcertLikeRepository extends JpaRepository<ConcertLike, Long>
1919

2020
List<ConcertLike> getConcertLikesByConcert(Concert concert);
2121

22+
// Fetch 사용해서 N+1 문제 해결
2223
@Query("""
2324
SELECT
2425
cl
2526
FROM
2627
ConcertLike cl
28+
JOIN FETCH
29+
cl.user u
2730
JOIN
28-
cl.concert c
31+
cl.concert c
2932
WHERE
3033
c.ticketTime
3134
BETWEEN

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/repository/TicketOfficeRepository.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,5 +28,8 @@ void deleteByConcertId(
2828
@Param("concertId")
2929
Long concertId);
3030

31+
@Query("SELECT t FROM TicketOffice t JOIN t.concert c WHERE c IN :concerts")
32+
List<TicketOffice> findAllByConcerts(@Param("concerts") List<Concert> concerts);
33+
3134
List<TicketOffice> getTicketOfficesByConcert_ConcertId(Long concertConcertId);
3235
}

src/main/java/com/back/web7_9_codecrete_be/domain/concerts/service/ConcertNotifyService.java

Lines changed: 169 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import com.back.web7_9_codecrete_be.domain.users.entity.User;
1111
import com.back.web7_9_codecrete_be.domain.users.repository.UserRepository;
1212
import lombok.RequiredArgsConstructor;
13+
import lombok.extern.slf4j.Slf4j;
1314
import org.springframework.scheduling.annotation.EnableScheduling;
1415
import org.springframework.scheduling.annotation.Scheduled;
1516
import org.springframework.stereotype.Service;
@@ -23,6 +24,7 @@
2324
import java.util.List;
2425
import java.util.Map;
2526

27+
@Slf4j
2628
@Service
2729
@EnableScheduling
2830
@RequiredArgsConstructor
@@ -45,13 +47,26 @@ private List<Concert> getTodayTicketingConcerts() {
4547

4648

4749
private Map<Long, List<TicketOffice>> getAllTicketOfficesMapFromConcerts(List<Concert> concerts) {
50+
// 예매처 정보를 빠르게 가져오기 위한 Map 지정
4851
Map<Long, List<TicketOffice>> ticketOfficeMap = new HashMap<>();
49-
List<TicketOffice> ticketOffices;
52+
//공연에 대한 모든 예매처 정보를 가져와서 저장
53+
List<TicketOffice> ticketOffices = ticketOfficeRepository.findAllByConcerts(concerts);
54+
55+
for (TicketOffice ticketOffice : ticketOffices) {
56+
log.info("예매처 조회");
57+
Long concertId = ticketOffice.getConcert().getConcertId();
58+
List<TicketOffice> ticketOfficeList = ticketOfficeMap.getOrDefault(concertId, new ArrayList<>());
59+
ticketOfficeList.add(ticketOffice);
60+
ticketOfficeMap.put(concertId, ticketOfficeList);
61+
}
62+
5063
// 줄일 수 없긴 개뿔, 가능하네.
64+
/*
5165
for (Concert concert : concerts) {
5266
ticketOffices = ticketOfficeRepository.getTicketOfficesByConcert(concert);
5367
ticketOfficeMap.put(concert.getConcertId(), ticketOffices);
5468
}
69+
*/
5570
// 예매처 맵 반환
5671
return ticketOfficeMap;
5772
}
@@ -65,6 +80,7 @@ private Map<String, List<Long>> getSendingEmailFromLikeUser(List<Concert> concer
6580

6681
List<ConcertLike> concertLikes = concertLikeRepository.getTodayConcertTicketingLikes(startOfToday, endOfToday);
6782
for (ConcertLike concertLike : concertLikes) {
83+
log.info("사용자 email 조회");
6884
// map에 해당 사용자 email의 ConcertId list 가져오기, 없다면 새로은 arraylist 사용
6985
List<Long> tempList = emailMap.getOrDefault(concertLike.getUser().getEmail(), new ArrayList<>());
7086
// 임시 리스트에 concertId 추가
@@ -110,30 +126,46 @@ public String sendTodayTicketingConcertsNotifyingEmail() {
110126

111127
for (String targetEmail : emailMap.keySet()) {
112128

113-
StringBuilder sb = new StringBuilder();
114-
115-
//위 타이틀 부분
116-
sb.append("""
117-
<!doctype html>
118-
<html lang="ko">
119-
<body style="margin:0;padding:0;background-color:#fafafa;
120-
font-family:-apple-system,BlinkMacSystemFont,system-ui,Roboto,Helvetica Neue,Segoe UI,Apple SD Gothic Neo,Noto Sans KR,Malgun Gothic,sans-serif;">
121-
122-
<div style="max-width:680px;margin:40px auto;background:#ffffff;
123-
border-radius:16px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
124-
125-
<!-- Header -->
126-
<div style="padding:32px;background:#1a1a1a;color:#ffffff;">
127-
<h1 style="margin:0 0 8px 0;font-size:26px;font-weight:700;">
128-
🎟 %s 오늘의 공연 예매 알림
129-
</h1>
130-
<div style="font-size:14px;opacity:0.85;">
131-
예매 시작 공연을 알려드립니다.
132-
</div>
133-
</div>
134-
135-
<div style="padding:32px;">
129+
StringBuilder htmlStringBuilder = new StringBuilder();
130+
StringBuilder textStringBuilder = new StringBuilder();
131+
132+
// 전체 타이틀 부분(html)
133+
htmlStringBuilder.append("""
134+
<!doctype html>
135+
<html lang="ko">
136+
<head>
137+
<meta charset="UTF-8" />
138+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
139+
<title>공연 예매 알림</title>
140+
</head>
141+
<body style="margin:0;padding:0;background-color:#fafafa;
142+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Malgun Gothic',
143+
'Apple SD Gothic Neo','Noto Sans KR',sans-serif;line-height:1.5;">
144+
145+
<div style="max-width:680px;margin:40px auto;background:#ffffff;
146+
border-radius:16px;overflow:hidden;box-shadow:0 2px 8px rgba(0,0,0,0.08);">
147+
148+
<!-- Header -->
149+
<div style="padding:32px;background:linear-gradient(135deg,#1a1a1a 0%%,#2d2d2d 100%%);
150+
color:#ffffff;">
151+
<h1 style="margin:0 0 8px 0;font-size:26px;font-weight:700;letter-spacing:-0.5px;">
152+
🎟 %s 오늘의 공연 예매 알림
153+
</h1>
154+
<div style="font-size:14px;opacity:0.85;">
155+
예매 시작 공연을 알려드립니다.
156+
</div>
157+
</div>
158+
159+
<div style="padding:32px;">
160+
""".formatted(today));
161+
//전체 타이틀 부분(text);
162+
textStringBuilder.append("""
163+
[NCB] 공연 예매 알림입니다.
164+
%s 오늘의 공연 예매 알림
165+
예매 시작 공연을 알려드립니다.
166+
---------------------------------------------------------
136167
""".formatted(today));
168+
137169
// 개별 공연 내용 작성
138170
for (Long concertId : emailMap.get(targetEmail)) {
139171

@@ -144,78 +176,142 @@ public String sendTodayTicketingConcertsNotifyingEmail() {
144176
posterImage = "https://via.placeholder.com/640x360?text=No+Image";
145177
}
146178

147-
sb.append("""
148-
<div style="border:1px solid #e8e8e8;border-radius:12px;
149-
padding:24px;margin-bottom:20px;">
150-
<img
151-
src="%s"
152-
alt="공연 포스터"
153-
style="width:100%%;height:200px;
154-
object-fit:cover;border-radius:8px;
155-
margin-bottom:16px;"
156-
/>
179+
// 날짜 정보 파싱 (예: 2025년 12월 17일 → 17, DEC)
180+
String day = concert.getTicketTime().format(DateTimeFormatter.ofPattern("dd"));
181+
String month = concert.getTicketTime().format(DateTimeFormatter.ofPattern("MMM", java.util.Locale.ENGLISH)).toUpperCase();
182+
183+
// 개별 공연 HTML 시작부
184+
htmlStringBuilder.append("""
185+
<!-- 공연 카드 시작 -->
186+
<div style="border:1px solid #e8e8e8;border-radius:12px;
187+
padding:24px;margin-bottom:20px;background:#ffffff;">
188+
189+
<!-- 포스터 이미지 -->
190+
<img src="%s" alt="공연 포스터"
191+
style="display:block;width:100%%;height:200px;
192+
object-fit:cover;border-radius:8px;margin-bottom:16px;" />
193+
194+
<!-- 공연 헤더 -->
195+
<div style="display:flex;align-items:flex-start;gap:20px;margin-bottom:16px;">
196+
197+
<!-- 날짜 박스 -->
198+
<div style="width:64px;text-align:center;padding:12px 0;
199+
background:#f5f5f5;border-radius:8px;flex-shrink:0;">
200+
<div style="font-size:28px;font-weight:700;color:#1a1a1a;line-height:1;">
201+
%s
202+
</div>
203+
<div style="font-size:13px;color:#666;margin-top:4px;font-weight:500;">
204+
%s
205+
</div>
206+
</div>
207+
208+
<!-- 공연 정보 -->
209+
<div style="flex:1;">
157210
<div style="font-size:16px;font-weight:600;color:#1a1a1a;margin-bottom:8px;">
158211
%s
159212
</div>
160-
<div style="font-size:13px;color:#666;margin-bottom:12px;">
161-
⏰ 예매 시간 :
162-
<strong>%s</strong>
213+
<div style="font-size:12px;color:#666;">
214+
⏰ %s
163215
</div>
164-
""".formatted(
216+
</div>
217+
</div>
218+
""".formatted(
165219
posterImage,
220+
day,
221+
month,
166222
concert.getName(),
167223
concert.getTicketTime()
168224
.format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분"))
169225
));
226+
// 개별 공연 text 시작부
227+
textStringBuilder.append("""
228+
공연명: %s
229+
티켓팅 시간: %s
230+
231+
""".formatted(concert.getName(), concert.getTicketTime().format(DateTimeFormatter.ofPattern("yyyy년 MM월 dd일 HH시 mm분"))));
232+
233+
// 공연 예매처 반복 처리
170234
for (TicketOffice ticketOffice : ticketOfficesMap.get(concertId)) {
171-
sb.append("""
172-
<div style="background:#f8f8f8;padding:12px 16px;
173-
border-radius:8px;margin-bottom:8px;font-size:13px;">
174-
175-
<div style="font-weight:600;color:#1a1a1a;">
235+
// 개별 예매처 html
236+
htmlStringBuilder.append("""
237+
<!-- 예매처 -->
238+
<div style="background:#f8f8f8;padding:16px;border-radius:8px;margin-bottom:8px;">
239+
<div style="display:flex;justify-content:space-between;
240+
align-items:center;flex-wrap:wrap;gap:12px;">
241+
242+
<div style="flex:1;min-width:150px;">
243+
<div style="font-size:12px;color:#888;margin-bottom:4px;">
244+
예매처
245+
</div>
246+
<div style="font-size:14px;font-weight:600;color:#1a1a1a;">
176247
%s
177248
</div>
178-
179-
<a href="%s" target="_blank"
180-
style="color:#1a1a1a;text-decoration:underline;font-size:12px;">
181-
예매 페이지 바로가기
182-
</a>
183249
</div>
184-
""".formatted(
250+
251+
<a href="%s" target="_blank"
252+
style="display:inline-block;padding:8px 24px;
253+
background:#1a1a1a;color:#ffffff;text-decoration:none;
254+
border-radius:6px;font-size:14px;font-weight:600;">
255+
예매하기
256+
</a>
257+
</div>
258+
</div>
259+
""".formatted(
185260
ticketOffice.getTicketOfficeName(),
186261
ticketOffice.getTicketOfficeUrl()
187262
));
263+
// 개별 예매처 text
264+
textStringBuilder.append("""
265+
예매처: %s
266+
예매링크: %s
267+
""".formatted(ticketOffice.getTicketOfficeName(), ticketOffice.getTicketOfficeUrl()));
188268
}
189269

190-
sb.append("</div>");
270+
htmlStringBuilder.append("</div>"); // 공연 카드 종료
271+
textStringBuilder.append("""
272+
---------------------------------------------------------
273+
"""); // text 자름
191274
}
192-
sb.append("""
193-
<div style="background:#f8f8f8;padding:20px;border-radius:8px;margin-top:24px;">
194-
<div style="font-size:14px;font-weight:bold;margin-bottom:6px;">
195-
ℹ️ 유의사항
196-
</div>
197-
<div style="font-size:12px;color:#666;">
198-
공연 정보는 각 공연의 상황에 따라 변경될 수 있으니
199-
예매 전 반드시 확인해주세요.
200-
</div>
201-
</div>
202-
203-
</div>
204-
205-
<div style="text-align:center;padding:32px;background:#fafafa;
206-
color:#999;font-size:12px;line-height:1.6;">
207-
이 메일은 자동으로 발송되었습니다.<br/>
208-
© 2025 Concert Notification Service
275+
276+
// 공연 마지막 바닥 부분 처리
277+
htmlStringBuilder.append("""
278+
<!-- 유의사항 -->
279+
<div style="background:#f8f8f8;padding:20px 24px;margin-top:24px;border-radius:8px;">
280+
<div style="display:flex;align-items:center;gap:6px;
281+
font-size:14px;font-weight:600;color:#1a1a1a;margin-bottom:8px;">
282+
⚠️ 유의사항
209283
</div>
210-
284+
<div style="font-size:12px;color:#666;line-height:1.6;">
285+
공연 정보는 각 공연의 상황에 따라 변경될 수 있으니
286+
예매 전 반드시 확인해주세요.
211287
</div>
212-
</body>
213-
</html>
288+
</div>
289+
290+
</div>
291+
292+
<!-- Footer -->
293+
<div style="text-align:center;padding:32px;background:#fafafa;
294+
color:#999;font-size:12px;line-height:1.6;">
295+
이 메일은 자동으로 발송되었습니다.<br/>
296+
© 2025 Concert Notification Service
297+
</div>
298+
299+
</div>
300+
</body>
301+
</html>
302+
""");
303+
textStringBuilder.append("""
304+
유의사항 : 공연 정보는 각 공연의 상황에 따라 변경될 수 있으니 예매 전 반드시 확인해주세요.
214305
""");
215306

216-
String contents = sb.toString();
217-
emailService.sendNotifyEmail(targetEmail, contents);
307+
308+
String htmlContent = htmlStringBuilder.toString();
309+
String textContent = textStringBuilder.toString();
310+
emailService.sendNotifyEmail(targetEmail, htmlContent,textContent);
218311
}
219-
return totalConcertsCount + "건의 공연을" + totalEmailCount + "명의 사용자에게 전송했습니다.";
312+
log.info("일일 공연 예매 오픈 알림 : " + totalConcertsCount + "건의 공연을 " + totalEmailCount + "명의 사용자에게 전송했습니다.");
313+
return totalConcertsCount + "건의 공연을 " + totalEmailCount + "명의 사용자에게 전송했습니다.";
220314
}
221315
}
316+
317+

0 commit comments

Comments
 (0)