-
Notifications
You must be signed in to change notification settings - Fork 0
[feat] 신고 시 이메일 전송 및 어드민 신고관리 API 구현 #134
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| package teamficial.teamficial_be.domain.admin.controller; | ||
|
|
||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.security.core.annotation.AuthenticationPrincipal; | ||
| import org.springframework.web.bind.annotation.*; | ||
| import teamficial.teamficial_be.domain.admin.dto.ReportResponseDto; | ||
| import teamficial.teamficial_be.domain.admin.service.AdminReportService; | ||
| import teamficial.teamficial_be.global.apiPayload.ApiResponse; | ||
| import teamficial.teamficial_be.global.security.AuthDetails; | ||
| import teamficial.teamficial_be.global.util.ScrollResponse; | ||
|
|
||
| @Tag(name = "어드민 신고 관리 관련 API") | ||
| @RestController | ||
| @RequestMapping("/admin") | ||
| @RequiredArgsConstructor | ||
| public class AdminReportController { | ||
|
|
||
| private final AdminReportService adminReportService; | ||
|
|
||
| @GetMapping("/report/{reportId}") | ||
| @Operation(summary = "신고된 키워드 코멘트 조회하기", description = "어드민이 신고된 키워드를 조회할 때 사용하는 API입니다.") | ||
| public ApiResponse<ReportResponseDto> getReport(@AuthenticationPrincipal AuthDetails authDetails, @PathVariable Long reportId){ | ||
|
|
||
| ReportResponseDto responseDto = adminReportService.getReport(authDetails.user(), reportId); | ||
|
|
||
| return ApiResponse.onSuccess(responseDto); | ||
| } | ||
|
|
||
| @GetMapping("/report") | ||
| @Operation(summary = "신고된 키워드 코멘트 리스트 조회하기", description = "어드민이 신고된 키워드를 조회할 때 사용하는 API입니다.") | ||
| public ApiResponse<ScrollResponse<ReportResponseDto>> getReportList(@AuthenticationPrincipal AuthDetails authDetails, | ||
| @RequestParam(defaultValue = "0") int page, | ||
| @RequestParam(defaultValue = "10") int size){ | ||
|
|
||
| ScrollResponse<ReportResponseDto> responseDtos = adminReportService.getReportList(authDetails.user(), page, size); | ||
|
|
||
| return ApiResponse.onSuccess(responseDtos); | ||
| } | ||
|
|
||
| @PostMapping("/report/{reportId}") | ||
| @Operation(summary = "신고된 키워드 코멘트 삭제하기", description = "어드민이 신고된 키워드를 삭제할 때 사용하는 API입니다.") | ||
| public ApiResponse<String> acceptReport(@AuthenticationPrincipal AuthDetails authDetails, @PathVariable Long reportId){ | ||
|
|
||
| adminReportService.acceptReport(reportId); | ||
|
|
||
| return ApiResponse.onSuccess("신고가 반영되었습니다."); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| package teamficial.teamficial_be.domain.admin.dto; | ||
|
|
||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import teamficial.teamficial_be.domain.report.entity.Report; | ||
|
|
||
| @Getter | ||
| @Builder | ||
| public class ReportResponseDto { | ||
| @Schema(name= "신고 id") | ||
| Long reportId; | ||
| @Schema(name = "신고된 키워드 코멘트 id") | ||
| Long commentId; | ||
| @Schema(example = "신고 유형") | ||
| String reportType; | ||
| @Schema(example = "신고 유형 (기타)") | ||
| String reportTypeEtc; | ||
| @Schema(example = "신고 내용") | ||
| String reportContent; | ||
|
|
||
| public static ReportResponseDto of(Report report) { | ||
| return ReportResponseDto.builder() | ||
| .reportId(report.getId()) | ||
| .commentId(report.getReportedCommentId()) | ||
| .reportType(report.getReportType().getDescription()) | ||
| .reportTypeEtc(report.getReportEtc()) | ||
| .reportContent(report.getContent()) | ||
| .build(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,47 @@ | ||
| package teamficial.teamficial_be.domain.admin.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.PageRequest; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.data.domain.Sort; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import teamficial.teamficial_be.domain.admin.dto.ReportResponseDto; | ||
| import teamficial.teamficial_be.domain.report.entity.Report; | ||
| import teamficial.teamficial_be.domain.report.service.ReportService; | ||
| import teamficial.teamficial_be.domain.user.entity.User; | ||
| import teamficial.teamficial_be.global.util.ScrollResponse; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class AdminReportService { | ||
|
|
||
| private final ReportService reportService; | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public ReportResponseDto getReport(User user, Long reportId) { | ||
| Report report = reportService.getById(reportId); | ||
|
|
||
| return ReportResponseDto.of(report); | ||
| } | ||
|
|
||
|
|
||
| @Transactional(readOnly = true) | ||
| public ScrollResponse<ReportResponseDto> getReportList(User user, int page, int size) { | ||
| Pageable pageable = PageRequest.of(page, size, Sort.Direction.DESC, "createdAt"); | ||
|
|
||
| Slice<Report> reportList = reportService.getAllReport(pageable); | ||
|
|
||
| Slice<ReportResponseDto> slice = reportList.map(ReportResponseDto::of); | ||
|
|
||
| return ScrollResponse.of(slice); | ||
| } | ||
|
|
||
| @Transactional | ||
| public void acceptReport(Long reportId) { | ||
| Report report = reportService.getById(reportId); | ||
|
|
||
| reportService.acceptReport(report); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,15 @@ | ||
| package teamficial.teamficial_be.domain.report.repository; | ||
|
|
||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.data.jpa.repository.JpaRepository; | ||
| import org.springframework.data.jpa.repository.Query; | ||
| import teamficial.teamficial_be.domain.report.entity.Report; | ||
|
|
||
| public interface ReportRepository extends JpaRepository<Report, Long> { | ||
| boolean existsByReportedCommentId(Long keywordCommentId); | ||
|
|
||
| @Query("SELECT r FROM Report r " + | ||
| "WHERE r.isApplied = false ") | ||
| Slice<Report> findAllByIsApplied(Pageable pageable); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,46 @@ | ||
| package teamficial.teamficial_be.domain.report.service; | ||
|
|
||
| import jakarta.mail.MessagingException; | ||
| import jakarta.mail.internet.MimeMessage; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.core.io.ClassPathResource; | ||
| import org.springframework.mail.javamail.JavaMailSender; | ||
| import org.springframework.mail.javamail.MimeMessageHelper; | ||
| import org.springframework.stereotype.Service; | ||
| import org.thymeleaf.TemplateEngine; | ||
| import org.thymeleaf.context.Context; | ||
|
|
||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class MailService { | ||
|
|
||
| private final JavaMailSender mailSender; | ||
| private final TemplateEngine templateEngine; | ||
|
|
||
| public void sendReportEmail(String toEmail) { | ||
| Context context = new Context(); | ||
|
|
||
| String body = templateEngine.process("report", context); | ||
|
|
||
| sendHtmlEmail(toEmail, "[팀피셜] 신고가 접수되었습니다", body); | ||
| } | ||
|
|
||
| private void sendHtmlEmail(String to, String subject, String htmlBody) { | ||
| MimeMessage message = mailSender.createMimeMessage(); | ||
|
|
||
| try { | ||
| MimeMessageHelper helper = new MimeMessageHelper(message, true, "UTF-8"); | ||
| helper.setTo(to); | ||
| helper.setSubject(subject); | ||
| helper.setText(htmlBody, true); | ||
|
|
||
| ClassPathResource imageResource = new ClassPathResource("/static/report_email.jpeg"); | ||
| helper.addInline("report_email", imageResource); | ||
|
|
||
| mailSender.send(message); | ||
| } catch (MessagingException e) { | ||
| throw new RuntimeException(e); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,16 @@ | ||
| package teamficial.teamficial_be.domain.report.service; | ||
|
|
||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.data.domain.Pageable; | ||
| import org.springframework.data.domain.Slice; | ||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
| import teamficial.teamficial_be.domain.keyword.entity.HeadKeyword; | ||
| import teamficial.teamficial_be.domain.keyword.entity.Keyword; | ||
| import teamficial.teamficial_be.domain.keyword.entity.KeywordComment; | ||
| import teamficial.teamficial_be.domain.keyword.service.HeadKeywordService; | ||
| import teamficial.teamficial_be.domain.keyword.service.KeywordCommentService; | ||
| import teamficial.teamficial_be.domain.keyword.service.KeywordService; | ||
| import teamficial.teamficial_be.domain.report.dto.ReportRequestDto; | ||
| import teamficial.teamficial_be.domain.report.entity.Report; | ||
| import teamficial.teamficial_be.domain.report.entity.ReportType; | ||
|
|
@@ -19,11 +25,20 @@ public class ReportService { | |
|
|
||
| private final ReportRepository reportRepository; | ||
| private final KeywordCommentService keywordCommentService; | ||
| private final MailService mailService; | ||
| private final KeywordService keywordService; | ||
| private final HeadKeywordService headKeywordService; | ||
|
|
||
| @Transactional | ||
| public void reportTeamficialLog(User user, Long keywordCommentId, ReportRequestDto reportRequestDto) { | ||
| KeywordComment comment = keywordCommentService.getById(keywordCommentId); | ||
|
|
||
| //중복 신고 방지 | ||
| if (alreadyExistReport(keywordCommentId)){ | ||
| throw new GeneralException(ErrorStatus.REPORT_DUPLICATE); | ||
| } | ||
|
|
||
| //권한 확인 | ||
| if (!comment.getKeyword().getUser().getId().equals(user.getId())) { | ||
| throw new GeneralException(ErrorStatus._FORBIDDEN); | ||
| } | ||
|
|
@@ -32,12 +47,54 @@ public void reportTeamficialLog(User user, Long keywordCommentId, ReportRequestD | |
| .reportType(reportRequestDto.getReportType()) | ||
| .reportEtc(reportRequestDto.getReportType()== ReportType.OTHER? reportRequestDto.getReportEtc() : null) | ||
| .content(reportRequestDto.getContent()) | ||
| .reportedCommentId(keywordCommentId) | ||
| .isApplied(false) | ||
| .user(user) | ||
| .build(); | ||
|
|
||
| reportRepository.save(report); | ||
|
|
||
| //이메일 전송 로직 | ||
| mailService.sendReportEmail(user.getEmail()); | ||
|
Comment on lines
57
to
+58
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major 🧩 Analysis chain🏁 Script executed: # First, find the ReportService.java file and understand the context
find . -type f -name "ReportService.java" | head -20Repository: TEAMFICIAL/teamficial-be Length of output: 149 🏁 Script executed: # Also search for the mailService implementation
find . -type f -name "*MailService*" -o -name "*mail*" | grep -E "\.(java|kt)$" | head -20Repository: TEAMFICIAL/teamficial-be Length of output: 147 🏁 Script executed: # Search for @Async configuration in the project
rg "@Async|@EnableAsync|Executor" --type java | head -30Repository: TEAMFICIAL/teamficial-be Length of output: 165 🏁 Script executed: # Read ReportService.java to understand the context around lines 57-58
cat -n ./src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java | head -80Repository: TEAMFICIAL/teamficial-be Length of output: 3763 🏁 Script executed: # Read MailService.java to check the email implementation
cat -n ./src/main/java/teamficial/teamficial_be/domain/report/service/MailService.javaRepository: TEAMFICIAL/teamficial-be Length of output: 1898 🏁 Script executed: # Search for @EnableAsync configuration
rg "@EnableAsync" --type java -A 2 -B 2Repository: TEAMFICIAL/teamficial-be Length of output: 50 🏁 Script executed: # Search for other email sending patterns in the codebase
rg "mailService\.|MailService\.|sendEmail|@Async" --type java | grep -v "node_modules"Repository: TEAMFICIAL/teamficial-be Length of output: 299 🏁 Script executed: # Check the full MailService usage across the project
rg "sendReportEmail|sendHtmlEmail" --type java -B 2 -A 2Repository: TEAMFICIAL/teamficial-be Length of output: 1980 🏁 Script executed: # Look for event-driven or async patterns in the codebase
rg "ApplicationEvent|@EventListener|@Async|Executor" --type java -B 1 | head -50Repository: TEAMFICIAL/teamficial-be Length of output: 253 🏁 Script executed: # Search for Spring configuration files that might have @EnableAsync
find . -type f \( -name "*.java" -o -name "*.xml" -o -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) | xargs grep -l "EnableAsync\|AsyncConfigurer\|TaskExecutor" 2>/dev/null | head -20Repository: TEAMFICIAL/teamficial-be Length of output: 50 🏁 Script executed: # Check if there's a configuration class that enables async
fd ".*Config.*\.java$" --type f | xargs grep -l "@Configuration\|@EnableAsync\|Executor" 2>/dev/null | head -20Repository: TEAMFICIAL/teamficial-be Length of output: 667 🏁 Script executed: # Look at PreSignedUrlService to see how @Async is implemented there
cat -n ./src/main/java/teamficial/teamficial_be/domain/profile/service/PreSignedUrlService.java | grep -A 10 -B 5 "@Async"Repository: TEAMFICIAL/teamficial-be Length of output: 645 🏁 Script executed: # Check if @EnableAsync might be in the main application class
find . -type f -name "*Application.java" -o -name "Main.java" | xargs cat 2>/dev/null | head -50Repository: TEAMFICIAL/teamficial-be Length of output: 1603 🏁 Script executed: # Check Spring Boot configuration in application properties/yml files
find . -type f \( -name "application*.yml" -o -name "application*.yaml" -o -name "application*.properties" \) -exec cat {} \; 2>/dev/null | head -100Repository: TEAMFICIAL/teamficial-be Length of output: 50 🏁 Script executed: # Look for any async configuration that might exist
rg "TaskExecutor|ThreadPoolExecutor|@Bean.*Executor" --type java -B 3 -A 5Repository: TEAMFICIAL/teamficial-be Length of output: 50 🏁 Script executed: # Double-check if @EnableAsync is somewhere else or if there's a different async configuration
rg "@EnableAsync|@EnableAsyncSupport" --type javaRepository: TEAMFICIAL/teamficial-be Length of output: 50 🏁 Script executed: # Check the current transaction behavior - look for @Transactional scope
grep -n "@Transactional" ./src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.javaRepository: TEAMFICIAL/teamficial-be Length of output: 189 🌐 Web query:
💡 Result: Short answer:
Pointers / minimal examples:
Sources:
이메일 전송이 동기적으로 수행되며, @EnableAsync가 프로젝트에 구성되지 않았습니다.
비동기 처리를 구현하려면 다음과 같이 진행하세요:
메일 서버 타임아웃도 설정하여 무한 대기를 방지하세요. 🤖 Prompt for AI Agents |
||
| } | ||
|
|
||
| @Transactional | ||
| public void acceptReport(Report report) { | ||
|
|
||
| if (report.isApplied()) { | ||
| throw new GeneralException(ErrorStatus.REPORT_ALREADY_APPLIED); | ||
| } | ||
|
|
||
| //코멘트 삭제 | ||
| KeywordComment comment = keywordCommentService.getById(report.getReportedCommentId()); | ||
|
|
||
| keywordCommentService.delete(comment); | ||
|
|
||
| //신고된 코멘트로 인해 추출된 키워드의 코멘트가 신고된 코멘트밖에 없는 경우 | ||
| Keyword keyword = comment.getKeyword(); | ||
|
|
||
| keyword.decreaseCount(); | ||
| if (keyword.getCount() ==0){ | ||
| keywordService.delete(keyword); | ||
| } | ||
|
|
||
| report.reportAccept(); | ||
|
|
||
| reportRepository.save(report); | ||
| } | ||
|
dldusgh318 marked this conversation as resolved.
|
||
|
|
||
| @Transactional(readOnly = true) | ||
| public Report getById(Long reportId) { | ||
| return reportRepository.findById(reportId) | ||
| .orElseThrow(()-> new GeneralException(ErrorStatus.NOT_FOUND_REPORT)); | ||
| } | ||
|
|
||
| @Transactional(readOnly = true) | ||
| public Slice<Report> getAllReport(Pageable pageable) { | ||
| return reportRepository.findAllByIsApplied(pageable); | ||
| } | ||
|
|
||
| private boolean alreadyExistReport(Long keywordCommentId) { | ||
| return reportRepository.existsByReportedCommentId(keywordCommentId); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.