Skip to content

[feat] 신고 시 이메일 전송 및 어드민 신고관리 API 구현 - #134

Merged
dldusgh318 merged 3 commits into
developfrom
feature/report
Jan 13, 2026
Merged

[feat] 신고 시 이메일 전송 및 어드민 신고관리 API 구현#134
dldusgh318 merged 3 commits into
developfrom
feature/report

Conversation

@dldusgh318

@dldusgh318 dldusgh318 commented Jan 13, 2026

Copy link
Copy Markdown
Contributor

#️⃣연관된 이슈

close #129

📝작업 내용

유저가 팀피셜록을 신고할 경우, 이메일로 확인되었다고 전송하고, 어드민이 신고를 관리할 수 있게 API를 구현했습니다.

어드민 API는 권한 막아둔 것도 체크했습니다
스크린샷 2026-01-13 오후 6 28 14

  • 팀피셜록 신고 시 이메일 전송
    IMG_17C10DA9BACB-1
  • 어드민 신고 관리 관련 API
    • 신고 리스트 조회
스크린샷 2026-01-13 오후 6 36 17
- 신고 단건 조회
스크린샷 2026-01-13 오후 6 39 20
- 신고된 키워드 코멘트 삭제
스크린샷 2026-01-13 오후 6 39 31

💬리뷰 요구사항(선택)

추후 비동기 고려

Summary by CodeRabbit

  • 새로운 기능
    • 관리자용 신고 관리 UI/API 추가(신고 조회·목록·처리) 및 신고 처리 시 이메일 알림 기능 도입
  • 버그 수정
    • 동일 댓글에 대한 중복 신고 차단, 신고 권한 검증 강화, 신고 반영 시 관련 카운트·키워드 정리
  • 문서
    • 신고 관련 API 메타데이터(문서화) 개선
  • 기타
    • 키워드 카운트 음수 방지 로직 및 관리자 전용 보안 규칙 추가

✏️ Tip: You can customize this high-level summary in your review settings.

@dldusgh318 dldusgh318 self-assigned this Jan 13, 2026
@dldusgh318 dldusgh318 added the ✨ feature 기능 개발 label Jan 13, 2026
@coderabbitai

coderabbitai Bot commented Jan 13, 2026

Copy link
Copy Markdown

Caution

Review failed

The pull request is closed.

Walkthrough

관리자용 신고 관리 엔드포인트와 신고 생성/처리 로직, 이메일 알림 및 템플릿, 관련 리포지토리/엔티티/서비스 변경을 추가합니다. 보안 설정에 /admin/** ADMIN 권한 규칙과 JWT 필터 로깅 주석 제거·검증 흐름 정비도 포함됩니다.

Changes

Cohort / File(s) 변경 요약
관리자 신고 API
src/main/java/.../admin/controller/AdminReportController.java, src/main/java/.../admin/dto/ReportResponseDto.java, src/main/java/.../admin/service/AdminReportService.java
관리자용 신고 조회(단건/목록) 및 수락 엔드포인트 추가, Report -> ReportResponseDto 매핑 DTO 및 서비스 계층 추가.
신고 도메인 변경
src/main/java/.../report/entity/Report.java, src/main/java/.../report/repository/ReportRepository.java, src/main/java/.../report/service/ReportService.java
Report에 reportedCommentId 필드 및 reportAccept() 추가. 리포지토리에 existsByReportedCommentId 및 미처리 신고 조회(Slice) 추가. ReportService에 중복/권한 확인, 신고 저장 시 이메일 전송, 수락 시 댓글·키워드 삭제/카운트 조정 로직 추가.
이메일 및 템플릿
src/main/java/.../report/service/MailService.java, src/main/resources/templates/report.html, build.gradle
Thymeleaf·JavaMail 의존성 추가, HTML 템플릿과 MIME HTML 이메일 전송 서비스 구현(인라인 이미지 포함).
키워드/댓글 서비스 변경
src/main/java/.../keyword/entity/Keyword.java, src/main/java/.../keyword/service/KeywordService.java, src/main/java/.../keyword/service/KeywordCommentService.java
Keyword.decreaseCount() (음수 방지) 추가. KeywordService 및 KeywordCommentService에 delete(...) 위임 메서드 추가.
API 문서·에러 상태·컨트롤러 메타데이터
src/main/java/.../report/controller/ReportController.java, src/main/java/.../global/apiPayload/code/status/ErrorStatus.java
ReportController에 @Tag 어노테이션 추가. ErrorStatus에 REPORT_DUPLICATE, NOT_FOUND_REPORT, REPORT_ALREADY_APPLIED 상수 추가.
보안 설정 및 JWT 필터
src/main/java/.../global/config/SecurityConfig.java, src/main/java/.../global/security/jwt/JwtAuthenticationFilter.java
SecurityConfig에 /admin/** 경로에 ADMIN 권한 요구 규칙 추가. JwtAuthenticationFilter에 @Slf4j 추가 및 토큰 검증 흐름 활성화(유효성 검사 후 Authentication 설정).

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
Loading
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 (성공 메시지)
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed PR 제목이 주요 변경사항을 명확하게 요약하고 있습니다. 신고 시 이메일 전송과 어드민 신고관리 API 구현이라는 핵심 내용이 잘 반영되어 있습니다.
Linked Issues check ✅ Passed PR이 연결된 이슈 #129의 모든 요구사항을 충족합니다. 팀피셜록 신고 API, 이메일 전송 로직, 어드민 신고관리 기능이 모두 구현되었습니다.
Out of Scope Changes check ✅ Passed PR의 모든 변경사항이 신고 기능 관련 API 구현이라는 범위 내에 있습니다. 추가로 Keyword 삭제 메서드와 보안 설정 업데이트는 신고 기능 구현을 위한 필수 지원 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5f594f6 and 2cd04e6.

📒 Files selected for processing (3)
  • src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java
  • src/main/java/teamficial/teamficial_be/global/apiPayload/code/status/ErrorStatus.java
  • src/main/java/teamficial/teamficial_be/global/security/jwt/JwtAuthenticationFilter.java

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" 문자열이 하드코딩되어 있습니다. UserRole enum이 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 필드를 primitive int로 변경하거나, 엔티티 생성 시 기본값을 보장하는 것을 고려해주세요.

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_DUPLICATEREPORT4001을, NOT_FOUND_REPORTREPORT404를 사용하고 있습니다. 다른 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와 의존성이 있습니다.

HeadKeywordHeadKeywordService가 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

📥 Commits

Reviewing files that changed from the base of the PR and between faa3b6c and 5f594f6.

⛔ Files ignored due to path filters (1)
  • src/main/resources/static/report_email.jpeg is excluded by !**/*.jpeg
📒 Files selected for processing (16)
  • build.gradle
  • src/main/java/teamficial/teamficial_be/domain/admin/controller/AdminReportController.java
  • src/main/java/teamficial/teamficial_be/domain/admin/dto/ReportResponseDto.java
  • src/main/java/teamficial/teamficial_be/domain/admin/service/AdminReportService.java
  • src/main/java/teamficial/teamficial_be/domain/keyword/entity/Keyword.java
  • src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordCommentService.java
  • src/main/java/teamficial/teamficial_be/domain/keyword/service/KeywordService.java
  • src/main/java/teamficial/teamficial_be/domain/report/controller/ReportController.java
  • src/main/java/teamficial/teamficial_be/domain/report/entity/Report.java
  • src/main/java/teamficial/teamficial_be/domain/report/repository/ReportRepository.java
  • src/main/java/teamficial/teamficial_be/domain/report/service/MailService.java
  • src/main/java/teamficial/teamficial_be/domain/report/service/ReportService.java
  • src/main/java/teamficial/teamficial_be/global/apiPayload/code/status/ErrorStatus.java
  • src/main/java/teamficial/teamficial_be/global/config/SecurityConfig.java
  • src/main/java/teamficial/teamficial_be/global/security/jwt/JwtAuthenticationFilter.java
  • src/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!

페이지네이션 조회 및 중복 확인 로직이 적절하게 구현되어 있습니다.

Comment on lines 57 to +58
//이메일 전송 로직
mailService.sendReportEmail(user.getEmail());

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 -20

Repository: 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 -20

Repository: TEAMFICIAL/teamficial-be

Length of output: 147


🏁 Script executed:

# Search for @Async configuration in the project
rg "@Async|@EnableAsync|Executor" --type java | head -30

Repository: 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 -80

Repository: 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.java

Repository: TEAMFICIAL/teamficial-be

Length of output: 1898


🏁 Script executed:

# Search for @EnableAsync configuration
rg "@EnableAsync" --type java -A 2 -B 2

Repository: 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 2

Repository: 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 -50

Repository: 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 -20

Repository: 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 -20

Repository: 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 -50

Repository: 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 -100

Repository: 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 5

Repository: 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 java

Repository: 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.java

Repository: 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:

  1. Spring Framework — @EnableAsync / Async processing docs.
  2. Spring Boot — Task execution & scheduling (auto-configured AsyncTaskExecutor and applicationTaskExecutor details).
  3. JavaMailSender / Spring mail docs (JavaMailSender API is synchronous).
  4. Spring Framework integration docs — @async executor qualification and usage.
  5. Spring Boot Sending Email docs (timeout properties and auto-configuration).

이메일 전송이 동기적으로 수행되며, @EnableAsync가 프로젝트에 구성되지 않았습니다.

mailService.sendReportEmail()이 동기적으로 호출되어 이메일 전송이 완료될 때까지 요청 처리가 차단됩니다. 전송 실패 시 @transactional 범위 내에서 전체 트랜잭션이 롤백되며, 메일 서버 지연이 직접 API 응답 시간에 영향을 줍니다.

비동기 처리를 구현하려면 다음과 같이 진행하세요:

  1. @configuration 클래스에 @EnableAsync를 추가하고 TaskExecutor Bean을 구성
  2. MailService의 sendReportEmail 메서드에 @async 추가
  3. 또는 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.

@dldusgh318
dldusgh318 merged commit 52a9c08 into develop Jan 13, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ feature 기능 개발

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[feat] 신고 기능 관련 API

1 participant