Skip to content

Commit

Permalink
Merge pull request #216 from EFUB4-Jukebox/develop
Browse files Browse the repository at this point in the history
[Deploy] 배포 v0.6.7 - 신고 메일 전송 API 구현
  • Loading branch information
seohyun-lee authored Aug 19, 2024
2 parents 4df9687 + 686e4f1 commit bd38667
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 18 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
@AllArgsConstructor
public enum AlarmType {
FOLLOW("{0}(@{1})님이 팔로우했어요."),
REPORT("{0}(@{1})님에 대한 신고가 접수되었어요."),
DEFAULT("{0}(@{1})님이 보낸 알림이에요.")
;

Expand Down
19 changes: 18 additions & 1 deletion src/main/java/sws/songpin/domain/alarm/service/AlarmService.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,19 @@ public void createFollowAlarm(Member follower, Member following) {
alarmRepository.save(alarm);
emitterService.notify(following.getMemberId(), AlarmDefaultDataDto.from(true), "new alarm");
}
// 신고 접수 알림 생성
public void createReportAlarm(Member reporter, Member reported) {
String message = MessageFormat.format(AlarmType.REPORT.getMessagePattern(), reported.getNickname(), reported.getHandle());
Alarm alarm = Alarm.builder()
.alarmType(AlarmType.REPORT)
.message(message)
.sender(reported)
.receiver(reporter)
.isRead(false)
.build();
alarmRepository.save(alarm);
emitterService.notify(reporter.getMemberId(), AlarmDefaultDataDto.from(true), "new alarm");
}

public void deleteAllAlarmsOfMember(Member member){
alarmRepository.deleteAllBySender(member);
Expand All @@ -59,7 +72,11 @@ private List<AlarmUnitDto> getAndReadAlarms(Pageable pageable) {
Slice<Alarm> alarmSlice = alarmRepository.findByReceiverOrderByCreatedTimeDesc(currentMember, pageable);
if (alarmSlice != null && alarmSlice.hasContent()) {
for (Alarm alarm : alarmSlice) {
String message = MessageFormat.format(AlarmType.FOLLOW.getMessagePattern(), alarm.getSender().getNickname(), alarm.getSender().getHandle());
String message = switch (alarm.getAlarmType()){
case FOLLOW -> MessageFormat.format(AlarmType.FOLLOW.getMessagePattern(), alarm.getSender().getNickname(), alarm.getSender().getHandle());
case REPORT -> MessageFormat.format(AlarmType.REPORT.getMessagePattern(), alarm.getSender().getNickname(), alarm.getSender().getHandle());
case DEFAULT -> MessageFormat.format(AlarmType.DEFAULT.getMessagePattern(), alarm.getSender().getNickname(), alarm.getSender().getHandle());
};
alarmList.add(AlarmUnitDto.from(alarm, message));
alarm.readAlarm();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sws.songpin.domain.member.controller;
package sws.songpin.domain.email.controller;

import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
Expand All @@ -7,22 +7,31 @@
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import sws.songpin.domain.member.dto.request.EmailRequestDto;
import sws.songpin.domain.member.service.EmailService;
import sws.songpin.domain.email.dto.EmailRequestDto;
import sws.songpin.domain.email.dto.ReportRequestDto;
import sws.songpin.domain.email.service.EmailService;

@Tag(name = "Email", description = "Email 관련 API입니다.")
@RestController
@RequestMapping("/mail")
@RequiredArgsConstructor
public class EmailController {

private final EmailService emailService;

@Operation(summary = "비밀번호 재설정 이메일 전송", description = "요청받은 이메일로 비밀번호 재설정 링크 전달")
@PostMapping("/mail/pw")
@PostMapping("/pw")
public ResponseEntity<?> sendPasswordEmail(@RequestBody @Valid EmailRequestDto requestDto){
emailService.sendPasswordEmail(requestDto);
return ResponseEntity.ok().build();
}

@Operation(summary = "유저 신고 메일 전송", description = "유저 신고 내역을 담은 보고서를 운영자 메일로 전송")
@PostMapping("/report")
public ResponseEntity<?> sendReportEmail(@RequestBody @Valid ReportRequestDto requestDto){
emailService.sendReportEmail(requestDto);
return ResponseEntity.ok().build();
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sws.songpin.domain.member.dto.request;
package sws.songpin.domain.email.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotEmpty;
Expand Down
12 changes: 12 additions & 0 deletions src/main/java/sws/songpin/domain/email/dto/ReportRequestDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package sws.songpin.domain.email.dto;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import sws.songpin.domain.model.ReportType;

public record ReportRequestDto(
@NotNull Long reportedId,
@NotNull ReportType reportType,
@Size(max = 200) String reason
) {
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package sws.songpin.domain.member.service;
package sws.songpin.domain.email.service;

import jakarta.mail.MessagingException;
import lombok.RequiredArgsConstructor;
Expand All @@ -10,15 +10,20 @@
import org.springframework.transaction.annotation.Transactional;
import org.thymeleaf.context.Context;
import org.thymeleaf.spring6.SpringTemplateEngine;
import sws.songpin.domain.member.dto.request.EmailRequestDto;
import sws.songpin.domain.alarm.service.AlarmService;
import sws.songpin.domain.email.dto.EmailRequestDto;
import jakarta.mail.internet.MimeMessage;
import sws.songpin.domain.email.dto.ReportRequestDto;
import sws.songpin.domain.member.entity.Member;
import sws.songpin.domain.member.service.MemberService;
import sws.songpin.domain.model.ReportType;
import sws.songpin.global.auth.RedisService;
import sws.songpin.global.exception.CustomException;
import sws.songpin.global.exception.ErrorCode;


import javax.xml.catalog.Catalog;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.UUID;

Expand All @@ -37,20 +42,18 @@ public class EmailService {
private final JavaMailSender javaMailSender;
private final MemberService memberService;
private final RedisService redisService;
private final AlarmService alarmService;
private final SpringTemplateEngine templateEngine;

public void sendPasswordEmail(EmailRequestDto requestDto){

String email = requestDto.email();
String toMail = requestDto.email();

//요청받은 이메일로 가입한 회원이 탈퇴하거나 없는 경우 예외 처리
memberService.getActiveMemberByEmail(email);

memberService.getActiveMemberByEmail(toMail);

String uuid = UUID.randomUUID().toString().replaceAll("-", "");



String title = "[송핀] 비밀번호 재설정 링크 전송"; //이메일 제목
String link = passwordUrl + "/" + uuid;

Expand All @@ -62,16 +65,59 @@ public void sendPasswordEmail(EmailRequestDto requestDto){
String content = templateEngine.process("passwordResetMail.html", context);

//Redis 에 (UUID,Email) 쌍 저장
if(!redisService.setValuesWithTimeoutIfAbsent("password_uuid:"+uuid, email, Duration.ofMinutes(10))){
if(!redisService.setValuesWithTimeoutIfAbsent("password_uuid:"+uuid, toMail, Duration.ofMinutes(10))){
throw new CustomException(ErrorCode.ERROR);
}

//메일 전송
sendEmail(toMail,title,content);

}

public void sendReportEmail(ReportRequestDto requestDto){

Member reporter = memberService.getCurrentMember();
Member reported = memberService.getActiveMemberById(requestDto.reportedId());


Long reporterId = reporter.getMemberId();
Long reportedId = reported.getMemberId();
String reportedHandle = reported.getHandle();
ReportType reportType = requestDto.reportType();
String reason = requestDto.reason();

//신고자 ID 와 신고 대상 ID 가 동일한 경우
if(reporterId.equals(reportedId)){
throw new CustomException(ErrorCode.REPORT_BAD_REQUEST);
}

String title = "[유저 신고] " + reporterId + " → " + reportedId; //이메일 제목

HashMap<String,Object> map = new HashMap<>();
map.put("reporterId", reporterId);
map.put("reportedId", reportedId);
map.put("reportedUrl", "https://www.songpin.kr/users/"+reportedHandle);
map.put("reportType", reportType+"("+reportType.getMessage()+")");
map.put("reportTime", LocalDateTime.now());
map.put("reason", reason);

Context context = new Context();
context.setVariables(map); //템플릿에 전달할 데이터
String content = templateEngine.process("reportMail.html", context);

//메일 전송
sendEmail(fromMail,title,content);

//신고자에게 알림 전송
alarmService.createReportAlarm(reporter, reported);
}

private void sendEmail(String toMail, String title, String content){
try{
MimeMessage mailMessage = javaMailSender.createMimeMessage();
MimeMessageHelper helper = new MimeMessageHelper(mailMessage, true, "UTF-8");
helper.setFrom(fromMail); //이메일 발신 주소
helper.setTo(email); //이메일 송신 주소
helper.setTo(toMail); //이메일 송신 주소
helper.setSubject(title); //이메일 제목
helper.setText(content, true);
helper.addInline("logo", new ClassPathResource(logoPath));
Expand All @@ -80,8 +126,6 @@ public void sendPasswordEmail(EmailRequestDto requestDto){
} catch (MessagingException e){
throw new CustomException(ErrorCode.EMAIL_ERROR);
}


}

}
31 changes: 31 additions & 0 deletions src/main/java/sws/songpin/domain/model/ReportType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package sws.songpin.domain.model;

import com.fasterxml.jackson.annotation.JsonCreator;
import lombok.AllArgsConstructor;
import lombok.Getter;
import sws.songpin.global.exception.CustomException;
import sws.songpin.global.exception.ErrorCode;

@Getter
@AllArgsConstructor
public enum ReportType {

SPAM("스팸/홍보"),
FLOODING("도배"),
ABUSE("욕설/비방"),
DOXXING("개인정보 노출"),
OFFENSIVE("불쾌감 조성"),
OTHER("기타")
;

private final String message;

@JsonCreator
public static ReportType from(String s) {
try {
return ReportType.valueOf(s.toUpperCase());
} catch (IllegalArgumentException e) {
throw new CustomException(ErrorCode.INVALID_ENUM_VALUE);
}
}
}
1 change: 1 addition & 0 deletions src/main/java/sws/songpin/global/exception/ErrorCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public enum ErrorCode {
// 자신과 관련해 불가능한 요청
MEMBER_BAD_REQUEST(400, "자기 자신은 이 경로를 통해 조회할 수 없습니다."),
FOLLOW_BAD_REQUEST(400,"자기 자신은 팔로잉할 수 없습니다."),
REPORT_BAD_REQUEST(400, "자기 자신을 신고할 수 없습니다."),

// 401 Unauthorized
// 로그인 상태여야 하는 요청
Expand Down
66 changes: 66 additions & 0 deletions src/main/resources/templates/email/reportMail.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<!DOCTYPE html>
<html lang="ko" style="box-sizing: border-box;">
<head style="box-sizing: border-box;">
<meta charset="UTF-8" style="box-sizing: border-box;">
<title style="box-sizing: border-box;">Songpin Mail</title>
<link href="https://fonts.googleapis.com/css?family=Inter&display=swap" rel="stylesheet" style="box-sizing: border-box;">
</head>
<body style="margin: 0; padding: 0; font-family: 'Inter', sans-serif; background-color: #f4f4f4;">

<div style="width: 700px; margin: 0 auto; padding: 20px;">
<table style="width: 100%; border-collapse: collapse; background-color: #ffffff; box-shadow: 0px 0px 15px rgba(0, 0, 0, 0.1); border-radius: 8px; overflow: hidden;">
<tr>
<td colspan="5" style="text-align: center; padding: 20px; background-color: #4CAF50;">
<img src="cid:logo" alt="logo" style="max-width: 150px; height: auto;">
</td>
</tr>
<tr>
<td style="padding: 40px; text-align: center;">
<h2 style="font-size: 18px; color: #333333; margin-bottom: 10px;">유저 신고 보고서</h2>
<p style="font-size: 14px; color: #555555; margin-bottom: 30px;">아래는 유저 신고의 세부 정보입니다:</p>
<table style="width: 100%; border-collapse: collapse;">
<tr>
<td style="width: 300px; padding: 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee;">
<strong>신고자 ID:</strong>
</td>
<td style="padding: 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee;" th:text="${reporterId}">
</td>
</tr>
<tr>
<td style="width: 300px; padding: 15px; background-color: #ffffff; border-bottom: 1px solid #eeeeee;">
<strong>신고된 ID:</strong>
</td>
<td style="padding: 15px; background-color: #ffffff; border-bottom: 1px solid #eeeeee;" th:text="${reportedId}">
</td>
</tr>
<tr>
<td style="width: 300px; padding: 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee;">
<strong>신고 시간:</strong>
</td>
<td style="padding: 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee;" th:text="${reportTime}">
</td>
</tr>
<tr>
<td style="width: 300px; padding: 15px; background-color: #ffffff; border-bottom: 1px solid #eeeeee;">
<strong>신고 유형:</strong>
</td>
<td style="padding: 15px; background-color: #ffffff; border-bottom: 1px solid #eeeeee;" th:text="${reportType}">
</td>
</tr>
<tr>
<td style="width: 300px; padding: 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee;">
<strong>신고 사유:</strong>
</td>
<td style="padding: 25px 15px; background-color: #f9f9f9; border-bottom: 1px solid #eeeeee; height: auto; word-wrap: break-word; max-width: 500px;" th:text="${reason}">
</td>
</tr>
</table>
<div style="margin-top: 30px;">
<a th:href="${reportedUrl}" style="padding: 12px 25px; background-color: #4CAF50; color: #ffffff; text-decoration: none; border-radius: 5px; font-weight: bold; display: inline-block;">신고된 사용자 프로필로 이동</a>
</div>
</td>
</tr>
</table>
</div>
</body>
</html>

0 comments on commit bd38667

Please sign in to comment.