[feat] 신고 시 이메일 전송 및 어드민 신고관리 API 구현 - #134
Conversation
|
Caution Review failedThe pull request is closed. Walkthrough관리자용 신고 관리 엔드포인트와 신고 생성/처리 로직, 이메일 알림 및 템플릿, 관련 리포지토리/엔티티/서비스 변경을 추가합니다. 보안 설정에 /admin/** ADMIN 권한 규칙과 JWT 필터 로깅 주석 제거·검증 흐름 정비도 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant ReportController
participant ReportService
participant ReportRepository
participant MailService
participant KeywordService
User->>ReportController: POST /report (reportedCommentId, type, content)
ReportController->>ReportService: createReport(user, request)
ReportService->>ReportRepository: existsByReportedCommentId(commentId)
alt 이미 신고 존재
ReportService-->>ReportController: 오류 (REPORT_DUPLICATE)
else 신규 신고
ReportService->>ReportService: 신고자 권한 검증(키워드 소유 여부)
ReportService->>ReportRepository: save(report)
ReportRepository-->>ReportService: 저장된 Report
ReportService->>MailService: sendReportEmail(adminEmail)
MailService-->>ReportService: 전송 완료
ReportService-->>ReportController: 성공 응답
end
ReportController-->>User: ApiResponse
sequenceDiagram
participant Admin
participant AdminReportController
participant AdminReportService
participant ReportService
participant ReportRepository
participant KeywordCommentService
participant KeywordService
Admin->>AdminReportController: POST /admin/report/{id} (accept)
AdminReportController->>AdminReportService: acceptReport(id)
AdminReportService->>ReportService: getById(id)
ReportService->>ReportRepository: findById(id)
ReportRepository-->>ReportService: Report
AdminReportService->>ReportService: acceptReport(report)
ReportService->>KeywordCommentService: delete(reportedComment)
KeywordCommentService-->>ReportService: 삭제 완료
ReportService->>KeywordService: decreaseCount(관련 키워드)
KeywordService-->>ReportService: 카운트 감소(필요시 키워드 삭제)
ReportService->>ReportRepository: save(report with isApplied=true)
ReportRepository-->>ReportService: 저장 완료
AdminReportService-->>AdminReportController: 성공
AdminReportController-->>Admin: ApiResponse (성공 메시지)
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
📜 Recent review detailsConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Fix all issues with AI agents
In
@src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java:
- Around line 29-44: The sendHtmlEmail method in MailService is missing a sender
address: call helper.setFrom(...) (using a configured default from application
properties or injected config value) before sending so messages have a valid
From header and avoid spam/dispatch errors; also replace the generic
RuntimeException thrown in the catch block with a domain-specific unchecked
exception (e.g., MailSendException or a custom ReportEmailException) so callers
can handle mail failures explicitly while keeping the catch for
MessagingException intact.
In
@src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java:
- Around line 61-79: Add an idempotency guard at the start of acceptReport to
avoid double-processing: check the report's applied flag (e.g.,
report.isApplied() or equivalent) and return early (or throw) if it's already
true; only then proceed to fetch the reported comment via
report.getReportedCommentId(), call keywordCommentService.delete(comment),
adjust keyword via keyword.decreaseCount() and conditionally
keywordService.delete(keyword), call report.reportAccept(), and persist with
reportRepository.save(report) — this prevents duplicate deletions and repeated
keyword count decrements.
- Around line 57-58: ReportService currently calls
mailService.sendReportEmail(user.getEmail()) synchronously inside the
transactional flow, blocking request handling and risking rollback on mail
failures; enable asynchronous email sending by adding @EnableAsync to a
@Configuration class and define a TaskExecutor bean, annotate
MailService.sendReportEmail(...) with @Async (or convert to an ApplicationEvent
and an @Async @EventListener handler) so mail sending runs off the request
thread, and ensure mail client timeouts are configured to avoid indefinite
waits.
In
@src/main/java/teamficial/teamficial_be/global/security/jwt/JwtAuthenticationFilter.java:
- Around line 41-45: The current code calls authentication.getAuthorities()
before checking for null, risking an NPE; change the order to first retrieve and
null-check authentication (from getAuthentication()), only then call
authentication.getAuthorities(), and set the SecurityContext via
SecurityContextHolder.getContext().setAuthentication(authentication) if
non-null; also lower the logging level by replacing log.info("Authorities = {}",
authentication.getAuthorities()) with log.debug(...) to avoid noisy production
logs.
🧹 Nitpick comments (14)
src/main/java/teamficial/teamficial_be/global/config/SecurityConfig.java (2)
19-19: 사용되지 않는 import 제거 필요
UserRole이 import되었으나 파일 내에서 사용되지 않습니다. 67번 라인에서 하드코딩된 문자열"ADMIN"을 사용하고 있으므로, 이 import를 활용하거나 제거해야 합니다.
67-67:UserRole상수를 사용하여 하드코딩된 권한 문자열 제거 권장
"ADMIN"문자열이 하드코딩되어 있습니다.UserRoleenum이 import되어 있으므로 일관성을 위해UserRole.ADMIN.name()을 사용하는 것이 좋습니다. 이렇게 하면 enum 값이 변경될 때 동기화 문제를 방지할 수 있습니다.♻️ 권장 수정안
- .requestMatchers("/admin/**").hasAuthority("ADMIN") + .requestMatchers("/admin/**").hasAuthority(UserRole.ADMIN.name())src/main/java/teamficial/teamficial_be/domain/keyword/entity/Keyword.java (1)
42-46:count가 null일 경우 NullPointerException 발생 가능
count필드가Integer타입(line 32)이므로 null일 수 있습니다.this.count > 0비교 시 unboxing 과정에서 NPE가 발생할 수 있습니다.increaseCount()메서드도 동일한 문제가 있습니다.💡 Null-safe 처리 제안
public void decreaseCount() { - if (this.count > 0) { + if (this.count != null && this.count > 0) { this.count--; } }또는
count필드를 primitiveint로 변경하거나, 엔티티 생성 시 기본값을 보장하는 것을 고려해주세요.src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordService.java (1)
87-89:@Transactional어노테이션 누락다른 쓰기 메서드들(
saveKeyword,saveBestKeyword)과 일관성을 위해delete메서드에도@Transactional어노테이션 추가를 권장합니다.♻️ 수정 제안
+ @Transactional public void delete(Keyword keyword) { keywordRepository.delete(keyword); }src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordCommentService.java (1)
34-36:@Transactional어노테이션 추가 권장
KeywordService.delete()와 마찬가지로, 쓰기 작업에 대한 명시적인 트랜잭션 관리를 위해@Transactional어노테이션 추가를 권장합니다.♻️ 수정 제안
+ @Transactional public void delete(KeywordComment comment) { keywordCommentRepository.delete(comment); }src/main/resources/templates/report.html (1)
6-12: 이메일 클라이언트에서 외부 폰트가 렌더링되지 않을 수 있습니다.대부분의 이메일 클라이언트(Gmail, Outlook 등)는 보안상의 이유로
@font-face와 외부 폰트 로딩을 차단합니다. Pretendard 폰트가 적용되지 않을 가능성이 높으므로,font-family에 fallback 폰트를 지정하는 것을 권장합니다.💡 fallback 폰트 추가 제안
<style> @font-face { font-family: 'Pretendard'; src: url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/woff2/Pretendard-Regular.woff2') format('woff2'); font-weight: 400; } + body { + font-family: 'Pretendard', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; + } </style>src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java (1)
21-27: 이메일 발송을 비동기로 처리하는 것을 고려해 주세요.현재 구현은 동기 방식으로, 이메일 발송 중 SMTP 서버 응답 지연 시 사용자 요청이 블로킹됩니다.
@Async를 사용하면 사용자 응답 시간을 개선할 수 있습니다.♻️ 비동기 이메일 발송 제안
+import org.springframework.scheduling.annotation.Async; + @Service @RequiredArgsConstructor public class MailService { // ... + @Async public void sendReportEmail(String toEmail) { Context context = new Context(); String body = templateEngine.process("report", context); sendHtmlEmail(toEmail, "[팀피셜] 신고가 접수되었습니다", body); } }
@EnableAsync가 설정 클래스에 추가되어 있는지 확인해 주세요.src/main/java/teamficial/teamficial_be/global/apiPayload/code/status/ErrorStatus.java (1)
58-60: 에러 코드 네이밍 패턴이 일관적이지 않습니다.
REPORT_DUPLICATE는REPORT4001을,NOT_FOUND_REPORT는REPORT404를 사용하고 있습니다. 다른 NOT_FOUND 에러들(USER404, TOKEN404 등)과는 일관적이지만, 같은 신고 기능 내에서 코드 체계가 다릅니다.일관성을 위해
REPORT4041또는REPORT4002같은 패턴을 고려해 볼 수 있습니다. 현재 상태로도 동작에 문제는 없습니다.src/main/java/teamficial/teamficial_be/domain/admin/dto/ReportResponseDto.java (1)
11-20:@Schema어노테이션 사용이 일관적이지 않습니다.
name속성은 필드 이름을,example은 예시 값을 지정하는 용도입니다. 현재example에 설명이 들어가 있어 Swagger 문서에서 의도와 다르게 표시될 수 있습니다.♻️ @Schema 어노테이션 수정 제안
@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; + @Schema(description = "신고 id", example = "1") + private Long reportId; + @Schema(description = "신고된 키워드 코멘트 id", example = "123") + private Long commentId; + @Schema(description = "신고 유형", example = "SPAM") + private String reportType; + @Schema(description = "신고 유형 (기타)", example = "기타 사유 내용") + private String reportTypeEtc; + @Schema(description = "신고 내용", example = "부적절한 내용입니다") + private String reportContent;src/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.java (2)
22-27:user파라미터가 사용되지 않습니다.
getReport메서드에서User user파라미터를 받지만 실제로 사용하지 않습니다. 만약 관리자 권한 검증이 이 레이어에서 필요하지 않다면 파라미터를 제거하는 것이 좋습니다. 반대로 권한 검증이 필요하다면 관리자 여부를 확인하는 로직을 추가해야 합니다.♻️ 파라미터 제거 제안
@Transactional(readOnly = true) -public ReportResponseDto getReport(User user, Long reportId) { +public ReportResponseDto getReport(Long reportId) { Report report = reportService.getById(reportId); return ReportResponseDto.of(report); }
30-39:user파라미터가 사용되지 않습니다.
getReportList메서드에서도User user파라미터가 전달되지만 사용되지 않습니다. 일관성을 위해 위의getReport메서드와 동일하게 처리하세요.src/main/java/teamficial/teamficial_be/domain/report/repository/ReportRepository.java (1)
12-14: 메서드 이름이 실제 동작과 일치하지 않습니다.
findAllByIsApplied는 파라미터로isApplied값을 받을 것처럼 보이지만, 실제로는isApplied = false인 레코드만 조회합니다. 의도를 명확히 하기 위해 메서드 이름을 변경하는 것을 권장합니다.♻️ 메서드 이름 개선 제안
-@Query("SELECT r FROM Report r " + - "WHERE r.isApplied = false ") -Slice<Report> findAllByIsApplied(Pageable pageable); +@Query("SELECT r FROM Report r WHERE r.isApplied = false") +Slice<Report> findAllPendingReports(Pageable pageable);src/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.java (1)
42-49: HTTP 메서드 선택을 재고해 주세요.신고된 키워드 코멘트를 삭제하는 작업에
POST대신DELETE메서드를 사용하는 것이 RESTful 설계 원칙에 더 부합합니다. 또한,authDetails파라미터가 선언되어 있지만 사용되지 않습니다.♻️ HTTP 메서드 변경 제안
-@PostMapping("/report/{reportId}") +@DeleteMapping("/report/{reportId}") @Operation(summary = "신고된 키워드 코멘트 삭제하기", description = "어드민이 신고된 키워드를 삭제할 때 사용하는 API입니다.") -public ApiResponse<String> acceptReport(@AuthenticationPrincipal AuthDetails authDetails, @PathVariable Long reportId){ +public ApiResponse<String> acceptReport(@PathVariable Long reportId){ adminReportService.acceptReport(reportId); return ApiResponse.onSuccess("신고가 반영되었습니다."); }src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java (1)
8-8: 사용되지 않는 import와 의존성이 있습니다.
HeadKeyword와HeadKeywordService가 import 및 주입되었지만 실제로 사용되지 않습니다. 사용하지 않는 코드는 제거해 주세요.🧹 미사용 코드 제거 제안
-import teamficial.teamficial_be.domain.keyword.entity.HeadKeyword; import teamficial.teamficial_be.domain.keyword.entity.Keyword;private final MailService mailService; private final KeywordService keywordService; -private final HeadKeywordService headKeywordService;Also applies to: 29-30
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
src/main/resources/static/report_email.jpegis excluded by!**/*.jpeg
📒 Files selected for processing (16)
build.gradlesrc/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.javasrc/main/java/teamficial/teamficial_be/domain/admin/dto/ReportResponseDto.javasrc/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.javasrc/main/java/teamficial/teamficial_be/domain/keyword/entity/Keyword.javasrc/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordCommentService.javasrc/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordService.javasrc/main/java/teamficial/teamficial_be/domain/report/controller/ReportController.javasrc/main/java/teamficial/teamficial_be/domain/report/entity/Report.javasrc/main/java/teamficial/teamficial_be/domain/report/repository/ReportRepository.javasrc/main/java/teamficial/teamficial_be/domain/report/service/MailService.javasrc/main/java/teamficial/teamficial_be/domain/report/service/ReportService.javasrc/main/java/teamficial/teamficial_be/global/apiPayload/code/status/ErrorStatus.javasrc/main/java/teamficial/teamficial_be/global/config/SecurityConfig.javasrc/main/java/teamficial/teamficial_be/global/security/jwt/JwtAuthenticationFilter.javasrc/main/resources/templates/report.html
🧰 Additional context used
🧬 Code graph analysis (6)
src/main/java/teamficial/teamficial_be/domain/report/controller/ReportController.java (1)
src/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.java (1)
Tag(14-50)
src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java (1)
src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java (1)
Service(22-95)
src/main/java/teamficial/teamficial_be/global/security/jwt/JwtAuthenticationFilter.java (1)
src/main/java/teamficial/teamficial_be/global/redis/RedisService.java (1)
Slf4j(16-112)
src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java (4)
src/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.java (1)
Service(16-47)src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordCommentService.java (1)
Service(14-37)src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordService.java (1)
Service(20-90)src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java (1)
Service(14-46)
src/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.java (1)
src/main/java/teamficial/teamficial_be/domain/report/controller/ReportController.java (1)
Tag(17-45)
src/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.java (3)
src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordCommentService.java (1)
Service(14-37)src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordService.java (1)
Service(20-90)src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java (1)
Service(22-95)
🔇 Additional comments (13)
build.gradle (1)
31-31: LGTM!이메일 전송 및 Thymeleaf 템플릿 렌더링을 위한 의존성이 적절하게 추가되었습니다. Spring Boot BOM을 통한 버전 관리가 올바르게 적용되어 있습니다.
Also applies to: 73-74
src/main/java/teamficial/teamficial_be/domain/report/controller/ReportController.java (1)
4-4: LGTM!Swagger API 문서화를 위한
@Tag어노테이션이 적절하게 추가되었습니다.AdminReportController와 일관된 패턴으로 API 그룹화가 잘 되어 있습니다.Also applies to: 17-17
src/main/resources/templates/report.html (1)
14-31: LGTM!템플릿 구조가 간결하고, 인라인 이미지 참조(
cid:report_email)와 alt 텍스트가 적절하게 설정되어 있습니다.src/main/java/teamficial/teamficial_be/domain/report/entity/Report.java (1)
34-39: LGTM!
reportedCommentId를 외래 키 관계 대신 ID로 저장하는 방식은KeywordComment삭제 시 참조 무결성 문제를 피할 수 있어 적절합니다.
reportAccept()메서드는 멱등성(idempotent)을 가지므로 중복 호출에도 안전합니다.src/main/java/teamficial/teamficial_be/domain/admin/dto/ReportResponseDto.java (1)
22-30: LGTM!
of()팩토리 메서드가Report엔티티에서 DTO로의 매핑을 명확하게 수행합니다.src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java (1)
38-39: 인라인 이미지 리소스가 정상적으로 존재합니다.
src/main/resources/static/report_email.jpeg파일이 존재하므로 런타임에FileNotFoundException이 발생하지 않습니다.src/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.java (1)
41-46: LGTM!
acceptReport메서드는 적절하게 트랜잭션을 사용하고 ReportService에 위임합니다.src/main/java/teamficial/teamficial_be/domain/report/repository/ReportRepository.java (1)
10-10: LGTM!중복 신고 방지 로직을 위한 적절한 존재 여부 확인 메서드입니다.
src/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.java (3)
22-29: LGTM!단일 신고 조회 엔드포인트가 적절하게 구현되었습니다.
31-40: LGTM!페이지네이션을 포함한 신고 목록 조회 엔드포인트가 적절하게 구현되었습니다.
14-18:/admin경로의 권한 검증은 올바르게 설정되어 있습니다.SecurityConfig에서
.requestMatchers("/admin/**").hasAuthority("ADMIN")로/admin/**경로에 대한 관리자 권한 체크가 SecurityFilterChain 수준에서 글로벌하게 적용되고 있으므로, 컨트롤러에 추가적인@PreAuthorize어노테이션이 필요하지 않습니다. 관리자 전용 접근 제어가 올바르게 구현되어 있습니다.src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java (2)
81-85: LGTM!표준적인 조회 로직과 적절한 예외 처리가 구현되어 있습니다.
87-94: LGTM!페이지네이션 조회 및 중복 확인 로직이 적절하게 구현되어 있습니다.
| //이메일 전송 로직 | ||
| mailService.sendReportEmail(user.getEmail()); |
There was a problem hiding this comment.
🛠️ 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:
Spring Boot @Async configuration @EnableAsync requirements JavaMailSender synchronous blocking
💡 Result:
Short answer:
-
@EnableAsync (or an equivalent async config) is required to enable Spring’s @async processing; Spring Boot provides an auto-configured AsyncTaskExecutor but does not implicitly enable the @async annotation for you (unless some other dependencia/autoconfiguration does). Use @EnableAsync on a @configuration (or implement AsyncConfigurer / provide an Executor bean named taskExecutor/applicationTaskExecutor) to control which Executor @async will use. [1][2][4]
-
JavaMailSender.send(...) is a synchronous, blocking call (it delegates to the JavaMail implementation). If you don’t want the caller thread blocked, call mail-sending from an @async method (or submit to your Executor / message queue), and set SMTP timeouts (mail.smtp.connectiontimeout, mail.smtp.timeout, mail.smtp.writetimeout) so an unresponsive mail server won’t block threads forever. Spring Boot auto-configures JavaMailSender when spring.mail.* is present. [3][5]
Pointers / minimal examples:
- Enable async: @configuration @EnableAsync public class Cfg { @bean Executor taskExecutor() { ThreadPoolTaskExecutor t = new ThreadPoolTaskExecutor(); t.setCorePoolSize(10); t.setMaxPoolSize(50); t.initialize(); return t; } }. Use @async on your mail method. [1][2]
- Mail timeouts in application.properties: spring.mail.properties[mail.smtp.connectiontimeout]=5000 and spring.mail.properties[mail.smtp.timeout]=3000. [5]
Sources:
- Spring Framework — @EnableAsync / Async processing docs.
- Spring Boot — Task execution & scheduling (auto-configured AsyncTaskExecutor and applicationTaskExecutor details).
- JavaMailSender / Spring mail docs (JavaMailSender API is synchronous).
- Spring Framework integration docs — @async executor qualification and usage.
- Spring Boot Sending Email docs (timeout properties and auto-configuration).
이메일 전송이 동기적으로 수행되며, @EnableAsync가 프로젝트에 구성되지 않았습니다.
mailService.sendReportEmail()이 동기적으로 호출되어 이메일 전송이 완료될 때까지 요청 처리가 차단됩니다. 전송 실패 시 @transactional 범위 내에서 전체 트랜잭션이 롤백되며, 메일 서버 지연이 직접 API 응답 시간에 영향을 줍니다.
비동기 처리를 구현하려면 다음과 같이 진행하세요:
- @configuration 클래스에 @EnableAsync를 추가하고 TaskExecutor Bean을 구성
- MailService의 sendReportEmail 메서드에 @async 추가
- 또는 ApplicationEvent 기반 이벤트 처리로 메일 전송 분리
메일 서버 타임아웃도 설정하여 무한 대기를 방지하세요.
🤖 Prompt for AI Agents
In
@src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java
around lines 57 - 58, ReportService currently calls
mailService.sendReportEmail(user.getEmail()) synchronously inside the
transactional flow, blocking request handling and risking rollback on mail
failures; enable asynchronous email sending by adding @EnableAsync to a
@Configuration class and define a TaskExecutor bean, annotate
MailService.sendReportEmail(...) with @Async (or convert to an ApplicationEvent
and an @Async @EventListener handler) so mail sending runs off the request
thread, and ensure mail client timeouts are configured to avoid indefinite
waits.
#️⃣연관된 이슈
📝작업 내용
어드민 API는 권한 막아둔 것도 체크했습니다

💬리뷰 요구사항(선택)
Summary by CodeRabbit
✏️ Tip: You can customize this high-level summary in your review settings.