diff --git a/config b/config index 3c79e0fb..08d3d14f 160000 --- a/config +++ b/config @@ -1 +1 @@ -Subproject commit 3c79e0fb47b554890814383fce34fb95ddae0ea9 +Subproject commit 08d3d14fe26a414623c6e3799b0d54961efdfc91 diff --git a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java index 28588f9d..a5c092de 100644 --- a/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java +++ b/src/main/java/com/assu/server/domain/auth/service/SignUpServiceImpl.java @@ -30,18 +30,23 @@ import com.assu.server.domain.student.repository.StudentRepository; import com.assu.server.domain.student.service.StudentService; import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.infra.discord.DiscordNotifier; import com.assu.server.infra.s3.AmazonS3Manager; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.locationtech.jts.geom.Coordinate; import org.locationtech.jts.geom.GeometryFactory; import org.locationtech.jts.geom.Point; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; import java.util.List; import java.util.Optional; +@Slf4j @Service @RequiredArgsConstructor @Transactional @@ -64,6 +69,7 @@ public class SignUpServiceImpl implements SignUpService { private final StudentService studentService; private final PhoneAuthService phoneAuthService; private final CommonAuthRepository commonAuthRepository; + private final DiscordNotifier discordNotifier; private RealmAuthAdapter pickAdapter(AuthRealm realm) { return realmAuthAdapters.stream() @@ -122,7 +128,11 @@ public SignUpResponseDTO signupSsuStudent(StudentTokenSignUpRequestDTO req) { member.setProfile(student); // 6) 가입 시점 사용 가능 제휴 동기화 (자정 배치와 별개로 즉시 반영) - studentService.syncUserPapersForStudent(student.getId()); + try { + studentService.syncUserPapersForStudent(student.getId()); + } catch (Exception e) { + log.error("[SignUp] UserPaper 동기화 실패 studentId={}", student.getId(), e); + } // 7) JWT 토큰 발급 TokensDTO tokens = jwtUtil.issueTokens( @@ -159,7 +169,11 @@ private SignUpResponseDTO restoreWithdrawnStudent( studentRepository.save(student); // 탈퇴 기간 중 변동된 제휴를 반영한다 - studentService.syncUserPapersForStudent(student.getId()); + try { + studentService.syncUserPapersForStudent(student.getId()); + } catch (Exception e) { + log.error("[SignUp] UserPaper 동기화 실패 studentId={}", student.getId(), e); + } TokensDTO tokens = jwtUtil.issueTokens( member.getId(), @@ -227,6 +241,8 @@ public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFil linkStore(partner, info.name(), address, info.detailAddress(), lat, lng, point); + runAfterCommit(() -> discordNotifier.send("🔔 새 제휴업체 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요.")); + return SignUpResponseDTO.from(member, null); } @@ -411,6 +427,8 @@ public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile si .build()); member.setProfile(admin); + runAfterCommit(() -> discordNotifier.send("🔔 새 학생회 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요.")); + return SignUpResponseDTO.from(member, null); } @@ -485,4 +503,17 @@ private Point toPoint(Double lat, Double lng) { private String pickDisplayAddress(String road, String jibun) { return (road != null && !road.isBlank()) ? road : jibun; } + + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/backoffice/service/BackofficeMemberServiceImpl.java b/src/main/java/com/assu/server/domain/backoffice/service/BackofficeMemberServiceImpl.java index 5cb8b434..191d6ba1 100644 --- a/src/main/java/com/assu/server/domain/backoffice/service/BackofficeMemberServiceImpl.java +++ b/src/main/java/com/assu/server/domain/backoffice/service/BackofficeMemberServiceImpl.java @@ -17,6 +17,7 @@ import com.assu.server.domain.store.entity.Store; import com.assu.server.domain.store.repository.StoreRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.infra.discord.DiscordNotifier; import com.assu.server.infra.s3.AmazonS3Manager; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -24,6 +25,8 @@ import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.web.multipart.MultipartFile; import java.time.LocalDateTime; @@ -44,6 +47,7 @@ public class BackofficeMemberServiceImpl implements BackofficeMemberService { private final WithdrawalService withdrawalService; private final AmazonS3Manager amazonS3Manager; private final ProfileImageService profileImageService; + private final DiscordNotifier discordNotifier; @Override @Transactional(readOnly = true) @@ -93,6 +97,9 @@ public BackofficeMemberSummaryDTO approveMember(Long memberId) { admin.setSignVerifiedAt(now); } + String roleLabel = member.getRole() == UserRole.PARTNER ? "제휴업체" : "학생회"; + runAfterCommit(() -> discordNotifier.send("✅ " + roleLabel + " 회원가입이 승인되었습니다.")); + return BackofficeMemberSummaryDTO.from(member); } @@ -103,6 +110,10 @@ public BackofficeMemberSummaryDTO rejectMember(Long memberId) { assertPendingApproval(member); member.setIsActivated(ActivationStatus.INACTIVE); + + String roleLabel = member.getRole() == UserRole.PARTNER ? "제휴업체" : "학생회"; + runAfterCommit(() -> discordNotifier.send("❌ " + roleLabel + " 회원가입이 거절되었습니다.")); + return BackofficeMemberSummaryDTO.from(member); } @@ -310,6 +321,19 @@ private void assertApprovable(Member member) { } } + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } + private void activatePartnerStore(Partner partner) { Store store = storeRepository.findByPartner(partner) .orElseThrow(() -> { diff --git a/src/main/java/com/assu/server/domain/backoffice/service/BackofficeReportServiceImpl.java b/src/main/java/com/assu/server/domain/backoffice/service/BackofficeReportServiceImpl.java index a2290351..4e86c045 100644 --- a/src/main/java/com/assu/server/domain/backoffice/service/BackofficeReportServiceImpl.java +++ b/src/main/java/com/assu/server/domain/backoffice/service/BackofficeReportServiceImpl.java @@ -24,8 +24,11 @@ import com.assu.server.domain.suggestion.repository.SuggestionRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.GeneralException; +import com.assu.server.infra.discord.DiscordNotifier; import lombok.RequiredArgsConstructor; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; @Service @RequiredArgsConstructor @@ -36,6 +39,7 @@ public class BackofficeReportServiceImpl implements BackofficeReportService { private final SuggestionRepository suggestionRepository; private final ReportRepository reportRepository; private final ApplicationEventPublisher eventPublisher; + private final DiscordNotifier discordNotifier; @Override @Transactional(readOnly = true) @@ -68,6 +72,13 @@ public BackofficeReportResponseDTO updateReportStatus(Long reportId, BackofficeR reportRepository.save(report); + String statusLabel = switch (nextStatus) { + case PENDING -> "대기"; + case PROCESSED -> "처리 완료"; + case REJECTED -> "기각"; + }; + runAfterCommit(() -> discordNotifier.send("📋 신고 상태가 변경되었습니다: " + statusLabel)); + return BackofficeReportResponseDTO.from(report); } @@ -81,6 +92,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteReview(Long reviewId) } processRelatedReports(ReportTargetType.REVIEW, reviewId); + runAfterCommit(() -> discordNotifier.send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다.")); return BackofficeReportDTO.SoftDeleteResponseDTO.of(reviewId); } @@ -95,6 +107,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteSuggestion(Long sugge } processRelatedReports(ReportTargetType.SUGGESTION, suggestionId); + runAfterCommit(() -> discordNotifier.send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다.")); return BackofficeReportDTO.SoftDeleteResponseDTO.of(suggestionId); } @@ -111,6 +124,7 @@ public BackofficeReportDTO.RejectReportResponseDTO rejectReport(Long reportId) { report.updateStatus(ReportStatus.REJECTED); eventPublisher.publishEvent(new ReportProcessedEvent( report.getId(), report.getTargetType(), report.getTargetId(), ReportStatus.REJECTED)); + runAfterCommit(() -> discordNotifier.send("✅ 신고가 기각되었습니다.")); return BackofficeReportDTO.RejectReportResponseDTO.of(reportId); } @@ -133,6 +147,19 @@ public List getReports(boolean pending, b ); } + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } + private void processRelatedReports(ReportTargetType targetType, Long targetId) { List reports = reportRepository.findAllByTargetTypeAndTargetId(targetType, targetId); reports.forEach(report -> { diff --git a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java index d37b3d17..379c9e28 100644 --- a/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java +++ b/src/main/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImpl.java @@ -7,10 +7,13 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; - +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import jakarta.transaction.Transactional; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; import java.util.List; import java.util.Optional; @@ -21,6 +24,7 @@ public class DeviceTokenServiceImpl implements DeviceTokenService { private final DeviceTokenRepository deviceTokenRepository; private final MemberRepository memberRepository; + private final MeterRegistry meterRegistry; @Override public Long register(String token, Long memberId) { @@ -52,6 +56,7 @@ public Long register(String token, Long memberId) { .build(); deviceTokenRepository.save(newToken); + incrementAfterCommit(meterRegistry.counter("device.token.registered")); return newToken.getId(); } @@ -63,11 +68,25 @@ public void unregister(Long tokenId, Long memberId) { throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_OWNED); } deviceToken.setActive(false); + incrementAfterCommit(meterRegistry.counter("device.token.deleted")); }, () -> { throw new DatabaseException(ErrorStatus.DEVICE_TOKEN_NOT_FOUND); }); } + private void incrementAfterCommit(Counter counter) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + counter.increment(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + counter.increment(); + } + }); + } + @Override public void deactivateTokens(List invalidTokens) { List invalidEntities = deviceTokenRepository.findAllByTokenIn(invalidTokens); diff --git a/src/main/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImpl.java index 9188aecf..4e86c8a0 100644 --- a/src/main/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImpl.java +++ b/src/main/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImpl.java @@ -8,6 +8,7 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; +import com.assu.server.infra.discord.DiscordNotifier; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -15,6 +16,8 @@ import org.springframework.data.domain.Sort; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; @Service @Transactional @@ -22,6 +25,7 @@ public class BackofficeInquiryServiceImpl implements BackofficeInquiryService { private final InquiryRepository inquiryRepository; + private final DiscordNotifier discordNotifier; @Override @Transactional(readOnly = true) @@ -67,5 +71,19 @@ public void answer(Long inquiryId, String answerText) { } inquiry.answer(answerText); + runAfterCommit(() -> discordNotifier.send("💬 문의 답변이 등록되었습니다.")); + } + + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); } } \ No newline at end of file diff --git a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java index bbedcbb5..61375455 100644 --- a/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java +++ b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java @@ -1,5 +1,6 @@ package com.assu.server.domain.inquiry.service; +import com.assu.server.infra.discord.DiscordNotifier; import com.assu.server.domain.common.dto.PageResponseDTO; import com.assu.server.domain.inquiry.dto.InquiryCreateRequestDTO; import com.assu.server.domain.inquiry.dto.InquiryResponseDTO; @@ -11,18 +12,25 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; +import io.micrometer.core.instrument.Counter; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.*; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; @Service @Transactional @RequiredArgsConstructor -public class InquiryServiceImpl implements InquiryService { +public class +InquiryServiceImpl implements InquiryService { private final InquiryRepository inquiryRepository; private final MemberRepository memberRepository; + private final MeterRegistry meterRegistry; + private final DiscordNotifier discordNotifier; /** 문의 등록 */ @Override @@ -33,9 +41,37 @@ public Long create(InquiryCreateRequestDTO inquiryCreateRequestDTO, Long memberI Inquiry inquiry = Inquiry.create(member, inquiryCreateRequestDTO); inquiryRepository.save(inquiry); + incrementAfterCommit(meterRegistry.counter("inquiry.created")); + runAfterCommit(() -> discordNotifier.send("📬 새 문의가 접수되었습니다. 백오피스에서 확인해 주세요.")); return inquiry.getId(); } + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } + + private void incrementAfterCommit(Counter counter) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + counter.increment(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + counter.increment(); + } + }); + } + /** 문의 내역 조회 (status=all|waiting|answered) */ @Override @Transactional(readOnly = true) diff --git a/src/main/java/com/assu/server/domain/member/service/ProfileImageServiceImpl.java b/src/main/java/com/assu/server/domain/member/service/ProfileImageServiceImpl.java index b18e66b8..0b0d0d66 100644 --- a/src/main/java/com/assu/server/domain/member/service/ProfileImageServiceImpl.java +++ b/src/main/java/com/assu/server/domain/member/service/ProfileImageServiceImpl.java @@ -5,6 +5,7 @@ import com.assu.server.domain.member.repository.MemberRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.infra.s3.AmazonS3Manager; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; @@ -19,6 +20,7 @@ public class ProfileImageServiceImpl implements ProfileImageService{ private final MemberRepository memberRepository; private final AmazonS3Manager amazonS3Manager; + private final MeterRegistry meterRegistry; private static final long MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB private static final Set ALLOWED_EXTENSIONS = Set.of("jpg", "jpeg", "png", "gif"); @@ -39,7 +41,10 @@ public String updateProfileImage(Long memberId, MultipartFile image) { String oldKey = member.getProfileUrl(); if (oldKey != null && !oldKey.isBlank()) { try { amazonS3Manager.deleteFile(oldKey); } - catch (Exception e) { log.warn("이전 프로필 삭제 실패 key={}", oldKey, e); } + catch (Exception e) { + log.warn("이전 프로필 삭제 실패 key={}", oldKey, e); + meterRegistry.counter("member.profile.image.s3.delete.failure").increment(); + } } member.setProfileUrl(uploadedKey); diff --git a/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java b/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java index 38f818d3..f3b3e9b0 100644 --- a/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java +++ b/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java @@ -16,11 +16,14 @@ import com.assu.server.domain.report.event.ReportProcessedEvent; import com.assu.server.domain.student.entity.Student; import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.infra.discord.DiscordNotifier; import org.springframework.context.ApplicationEventPublisher; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; @Slf4j @Service @@ -32,6 +35,7 @@ public class ReportServiceImpl implements ReportService { private final ReviewRepository reviewRepository; private final SuggestionRepository suggestionRepository; private final ApplicationEventPublisher eventPublisher; + private final DiscordNotifier discordNotifier; @Override @Transactional @@ -69,6 +73,8 @@ public ReportResponseDTO.CreateReportResponse reportContent(Long reporterId, savedReport.getTargetId(), savedReport.getStatus())); + runAfterCommit(() -> discordNotifier.send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요.")); + return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); } @@ -112,9 +118,24 @@ public ReportResponseDTO.CreateReportResponse reportStudent(Long reporterId, savedReport.getTargetId(), savedReport.getStatus())); + runAfterCommit(() -> discordNotifier.send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요.")); + return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); } + private void runAfterCommit(Runnable task) { + if (!TransactionSynchronizationManager.isSynchronizationActive()) { + task.run(); + return; + } + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + task.run(); + } + }); + } + // 콘텐츠 신고 대상 검증 메서드 private void validateContentReportTarget(Long reporterId, ReportTargetType targetType, Long targetId) { switch (targetType) { diff --git a/src/main/java/com/assu/server/domain/student/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/student/service/StudentServiceImpl.java index d7082999..79380bd7 100644 --- a/src/main/java/com/assu/server/domain/student/service/StudentServiceImpl.java +++ b/src/main/java/com/assu/server/domain/student/service/StudentServiceImpl.java @@ -49,12 +49,15 @@ import com.assu.server.global.exception.DatabaseException; import com.assu.server.infra.s3.AmazonS3Manager; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; +@Slf4j @Service @RequiredArgsConstructor public class StudentServiceImpl implements StudentService { @@ -245,7 +248,7 @@ public List getRecommendCarouselPartner }).toList(); } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void syncUserPapersForStudent(Long studentId) { Student student = studentRepository.findById(studentId) @@ -287,6 +290,7 @@ public void syncUserPapersForStudent(Long studentId) { if (!newUserPapers.isEmpty()) { userPaperRepository.saveAll(newUserPapers); } + log.info("[UserPaper] studentId={} synced={}", studentId, newUserPapers.size()); } @Transactional public StudentResponseDTO.CheckStampResponseDTO addStamp(Long memberId) { diff --git a/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java new file mode 100644 index 00000000..d3f00152 --- /dev/null +++ b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java @@ -0,0 +1,39 @@ +package com.assu.server.infra.discord; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.web.client.RestClient; + +import java.util.Map; + +@Slf4j +@Component +public class DiscordNotifier { + + private final RestClient restClient; + + @Value("${discord.webhook.service-alert:}") + private String serviceAlertWebhookUrl; + + public DiscordNotifier(RestClient.Builder builder) { + this.restClient = builder.build(); + } + + public void send(String message) { + if (serviceAlertWebhookUrl.isBlank()) { + return; + } + try { + restClient.post() + .uri(serviceAlertWebhookUrl) + .contentType(MediaType.APPLICATION_JSON) + .body(Map.of("content", message)) + .retrieve() + .toBodilessEntity(); + } catch (Exception e) { + log.warn("[Discord] 알림 전송 실패 message={}", message, e); + } + } +} \ No newline at end of file diff --git a/src/test/java/com/assu/server/domain/auth/service/SignUpServiceImplTest.java b/src/test/java/com/assu/server/domain/auth/service/SignUpServiceImplTest.java index 8d484284..01facd89 100644 --- a/src/test/java/com/assu/server/domain/auth/service/SignUpServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/auth/service/SignUpServiceImplTest.java @@ -55,6 +55,7 @@ import com.assu.server.domain.student.repository.StudentRepository; import com.assu.server.domain.student.service.StudentService; import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.infra.discord.DiscordNotifier; import com.assu.server.infra.s3.AmazonS3Manager; @ExtendWith(MockitoExtension.class) @@ -101,6 +102,9 @@ class SignUpServiceImplTest { @Mock private CommonAuthRepository commonAuthRepository; + @Mock + private DiscordNotifier discordNotifier; + @Mock private CommonAuth commonAuth; @@ -116,7 +120,7 @@ void setUp() { memberRepository, studentRepository, partnerRepository, adminRepository, List.of(realmAuthAdapter), amazonS3Manager, jwtUtil, new GeometryFactory(), storeRepository, ssuAuthService, ssuAuthRepository, - studentService, phoneAuthService, commonAuthRepository); + studentService, phoneAuthService, commonAuthRepository, discordNotifier); } @Test diff --git a/src/test/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImplTest.java b/src/test/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImplTest.java index ccc42493..b3528f14 100644 --- a/src/test/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/deviceToken/service/DeviceTokenServiceImplTest.java @@ -13,6 +13,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import com.assu.server.domain.deviceToken.entity.DeviceToken; @@ -22,6 +23,8 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @ExtendWith(MockitoExtension.class) class DeviceTokenServiceImplTest { @@ -35,6 +38,9 @@ class DeviceTokenServiceImplTest { @Mock private MemberRepository memberRepository; + @Spy + private MeterRegistry meterRegistry = new SimpleMeterRegistry(); + private static final Long MEMBER_ID = 1L; private static final String TOKEN = "fcm-token-abc"; diff --git a/src/test/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImplTest.java b/src/test/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImplTest.java index 3ecaed38..fe654dd2 100644 --- a/src/test/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImplTest.java @@ -19,6 +19,7 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; +import com.assu.server.infra.discord.DiscordNotifier; @ExtendWith(MockitoExtension.class) class BackofficeInquiryServiceImplTest { @@ -29,6 +30,9 @@ class BackofficeInquiryServiceImplTest { @Mock private InquiryRepository inquiryRepository; + @Mock + private DiscordNotifier discordNotifier; + @Test @DisplayName("페이지 번호가 1 미만이면 PAGE_UNDER_ONE 예외가 발생한다") void getInquiries_PageUnderOne_ThrowsException() { diff --git a/src/test/java/com/assu/server/domain/inquiry/service/InquiryServiceImplTest.java b/src/test/java/com/assu/server/domain/inquiry/service/InquiryServiceImplTest.java index 8c32bc41..e89c7996 100644 --- a/src/test/java/com/assu/server/domain/inquiry/service/InquiryServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/inquiry/service/InquiryServiceImplTest.java @@ -13,6 +13,7 @@ import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.Spy; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageImpl; @@ -29,6 +30,9 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.global.exception.GeneralException; +import com.assu.server.infra.discord.DiscordNotifier; +import io.micrometer.core.instrument.MeterRegistry; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; @ExtendWith(MockitoExtension.class) class InquiryServiceImplTest { @@ -42,6 +46,12 @@ class InquiryServiceImplTest { @Mock private MemberRepository memberRepository; + @Spy + private MeterRegistry meterRegistry = new SimpleMeterRegistry(); + + @Mock + private DiscordNotifier discordNotifier; + private static final Long MEMBER_ID = 1L; @Test diff --git a/src/test/java/com/assu/server/domain/report/service/ReportServiceImplTest.java b/src/test/java/com/assu/server/domain/report/service/ReportServiceImplTest.java index fef82aea..683a367d 100644 --- a/src/test/java/com/assu/server/domain/report/service/ReportServiceImplTest.java +++ b/src/test/java/com/assu/server/domain/report/service/ReportServiceImplTest.java @@ -33,6 +33,7 @@ import com.assu.server.domain.suggestion.entity.Suggestion; import com.assu.server.domain.suggestion.repository.SuggestionRepository; import com.assu.server.global.apiPayload.code.status.ErrorStatus; +import com.assu.server.infra.discord.DiscordNotifier; @ExtendWith(MockitoExtension.class) class ReportServiceImplTest { @@ -55,6 +56,9 @@ class ReportServiceImplTest { @Mock private ApplicationEventPublisher eventPublisher; + @Mock + private DiscordNotifier discordNotifier; + private static final Long REPORTER_ID = 1L; private static final Long REVIEW_ID = 100L;