From 21add68c0089a277d58c7296ee506dc9e005733a Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:38:01 +0900 Subject: [PATCH 1/9] =?UTF-8?q?[FEAT/#487]=20Inquiry=20=C2=B7=20Student=20?= =?UTF-8?q?usable=20=C2=B7=20Device=20Token=20=EC=BB=A4=EC=8A=A4=ED=85=80?= =?UTF-8?q?=20=EB=8F=84=EB=A9=94=EC=9D=B8=20=EB=A9=94=ED=8A=B8=EB=A6=AD=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - InquiryServiceImpl에 inquiry.created Counter 추가 (문의 생성 시 증가) - StudentServiceImpl에 student.usable.queried Counter 추가 (사용 가능 제휴 조회 시 증가) - DeviceTokenServiceImpl에 device.token.registered Counter 추가 (신규 토큰 등록 시 증가) - DeviceTokenServiceImpl에 device.token.deleted Counter 추가 (토큰 비활성화 시 증가) - 쓰기 트랜잭션 내 카운터는 afterCommit 패턴 적용으로 롤백된 작업 미집계 --- .../service/DeviceTokenServiceImpl.java | 21 ++++++++++++++++++- .../inquiry/service/InquiryServiceImpl.java | 19 +++++++++++++++++ .../student/service/StudentServiceImpl.java | 3 +++ 3 files changed, 42 insertions(+), 1 deletion(-) 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/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java index bbedcbb5..d7e8eddc 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 @@ -11,10 +11,14 @@ 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 @@ -23,6 +27,7 @@ public class InquiryServiceImpl implements InquiryService { private final InquiryRepository inquiryRepository; private final MemberRepository memberRepository; + private final MeterRegistry meterRegistry; /** 문의 등록 */ @Override @@ -33,9 +38,23 @@ public Long create(InquiryCreateRequestDTO inquiryCreateRequestDTO, Long memberI Inquiry inquiry = Inquiry.create(member, inquiryCreateRequestDTO); inquiryRepository.save(inquiry); + incrementAfterCommit(meterRegistry.counter("inquiry.created")); return inquiry.getId(); } + 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/student/service/StudentServiceImpl.java b/src/main/java/com/assu/server/domain/student/service/StudentServiceImpl.java index d7082999..db4582c1 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 @@ -48,6 +48,7 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.infra.s3.AmazonS3Manager; +import io.micrometer.core.instrument.MeterRegistry; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; @@ -72,6 +73,7 @@ public class StudentServiceImpl implements StudentService { private final HomeCurationItemRepository homeCurationItemRepository; private final StoreRepository storeRepository; private final AmazonS3Manager amazonS3Manager; + private final MeterRegistry meterRegistry; @Override @Transactional public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { @@ -211,6 +213,7 @@ public List getUsablePartnership(Long m .build(); }).toList(); + meterRegistry.counter("student.usable.queried").increment(); return Boolean.FALSE.equals(all) ? result.stream().limit(2).toList() : result; } From 800faecc1d2f3454a9275d06a7a085261c97b742 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:51:51 +0900 Subject: [PATCH 2/9] =?UTF-8?q?[FEAT/#487]=20student.usable.queried=20?= =?UTF-8?q?=EB=A9=94=ED=8A=B8=EB=A6=AD=20=EC=A0=9C=EA=B1=B0=20=EB=B0=8F=20?= =?UTF-8?q?UserPaper=20=EB=8F=99=EA=B8=B0=ED=99=94=20=EB=A1=9C=EA=B7=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - student.usable.queried Counter 제거 (조회 횟수 집계 불필요) - syncUserPapersForStudent에 동기화 결과 로그 추가 (studentId, synced 건수) - syncUserPapersForStudent를 REQUIRES_NEW 전파로 변경하여 실패 시 독립 롤백 --- .../domain/student/service/StudentServiceImpl.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 db4582c1..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 @@ -48,14 +48,16 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; import com.assu.server.global.exception.DatabaseException; import com.assu.server.infra.s3.AmazonS3Manager; -import io.micrometer.core.instrument.MeterRegistry; 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 { @@ -73,7 +75,6 @@ public class StudentServiceImpl implements StudentService { private final HomeCurationItemRepository homeCurationItemRepository; private final StoreRepository storeRepository; private final AmazonS3Manager amazonS3Manager; - private final MeterRegistry meterRegistry; @Override @Transactional public StudentResponseDTO.CheckStampResponseDTO getStamp(Long memberId) { @@ -213,7 +214,6 @@ public List getUsablePartnership(Long m .build(); }).toList(); - meterRegistry.counter("student.usable.queried").increment(); return Boolean.FALSE.equals(all) ? result.stream().limit(2).toList() : result; } @@ -248,7 +248,7 @@ public List getRecommendCarouselPartner }).toList(); } - @Transactional + @Transactional(propagation = Propagation.REQUIRES_NEW) @Override public void syncUserPapersForStudent(Long studentId) { Student student = studentRepository.findById(studentId) @@ -290,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) { From 9278f3fdabdc39557f53cf296f7670bb29684256 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:52:46 +0900 Subject: [PATCH 3/9] =?UTF-8?q?[FIX/#487]=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=A4=91=20UserPaper=20=EB=8F=99=EA=B8=B0=ED=99=94?= =?UTF-8?q?=20=EC=8B=A4=ED=8C=A8=20=EC=8B=9C=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EB=A1=A4=EB=B0=B1=EB=90=98=EB=8A=94=20=EB=B2=84?= =?UTF-8?q?=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - syncUserPapersForStudent 호출부를 try-catch로 감싸 동기화 실패가 회원가입에 영향 없도록 처리 - 동기화 실패 시 log.error로 studentId와 예외 기록 --- .../domain/auth/service/SignUpServiceImpl.java | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) 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..128cdc87 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 @@ -32,6 +32,7 @@ import com.assu.server.global.apiPayload.code.status.ErrorStatus; 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; @@ -42,6 +43,7 @@ import java.util.List; import java.util.Optional; +@Slf4j @Service @RequiredArgsConstructor @Transactional @@ -122,7 +124,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 +165,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(), From 8ea1ba7461d8eaa060c047f0064f1aba53fd440a Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:17:51 +0900 Subject: [PATCH 4/9] =?UTF-8?q?[FEAT/#487]=20=EB=AC=B8=EC=9D=98=20?= =?UTF-8?q?=EC=A0=91=EC=88=98=20Discord=20=EC=95=8C=EB=A6=BC=20=EB=B0=8F?= =?UTF-8?q?=20=ED=94=84=EB=A1=9C=ED=95=84=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20?= =?UTF-8?q?S3=20=EC=82=AD=EC=A0=9C=20=EC=8B=A4=ED=8C=A8=20=EB=A9=94?= =?UTF-8?q?=ED=8A=B8=EB=A6=AD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiscordNotifier 컴포넌트 추가 (infra/discord) - InquiryServiceImpl: 문의 생성 후 afterCommit 시점에 Discord #서비스-알림 채널로 알림 전송 - ProfileImageServiceImpl: 이전 프로필 이미지 S3 삭제 실패 시 member.profile.image.s3.delete.failure 카운터 증가 - config/prod: discord.webhook.service-alert URL 추가 --- config | 2 +- .../inquiry/service/InquiryServiceImpl.java | 19 +++++++- .../service/ProfileImageServiceImpl.java | 7 ++- .../server/infra/discord/DiscordNotifier.java | 43 +++++++++++++++++++ 4 files changed, 68 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/assu/server/infra/discord/DiscordNotifier.java 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/inquiry/service/InquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/InquiryServiceImpl.java index d7e8eddc..dde06618 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; @@ -23,11 +24,13 @@ @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 @@ -39,9 +42,23 @@ public Long create(InquiryCreateRequestDTO inquiryCreateRequestDTO, Long memberI inquiryRepository.save(inquiry); incrementAfterCommit(meterRegistry.counter("inquiry.created")); + runAfterCommit(discordNotifier::sendInquiryAlert); 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(); 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/infra/discord/DiscordNotifier.java b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java new file mode 100644 index 00000000..11f671de --- /dev/null +++ b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java @@ -0,0 +1,43 @@ +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 sendInquiryAlert() { + send("📬 새 문의가 접수되었습니다. 백오피스에서 확인해 주세요."); + } + + private 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 From ece84ec8784e6c53cbf980223624d591b4184daa Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:25:22 +0900 Subject: [PATCH 5/9] =?UTF-8?q?[FEAT/#487]=20=EC=8B=A0=EA=B3=A0=20?= =?UTF-8?q?=EC=A0=91=EC=88=98=C2=B7=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85=20?= =?UTF-8?q?=EC=8A=B9=EC=9D=B8=20=EB=8C=80=EA=B8=B0=20Discord=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiscordNotifier에 sendReportAlert, sendSignupPendingAlert 메서드 추가 - ReportServiceImpl: reportContent/reportStudent 저장 후 afterCommit으로 Discord 알림 발송 - SignUpServiceImpl: signupPartner(제휴업체)/signupAdmin(학생회) 신규 가입 시 afterCommit으로 Discord 알림 발송 (복구 케이스 제외) --- .../auth/service/SignUpServiceImpl.java | 21 +++++++++++++++++++ .../report/service/ReportServiceImpl.java | 21 +++++++++++++++++++ .../server/infra/discord/DiscordNotifier.java | 8 +++++++ 3 files changed, 50 insertions(+) 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 128cdc87..a6e8d4f1 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,6 +30,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; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -38,6 +39,8 @@ 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; @@ -66,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() @@ -237,6 +241,8 @@ public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFil linkStore(partner, info.name(), address, info.detailAddress(), lat, lng, point); + runAfterCommit(() -> discordNotifier.sendSignupPendingAlert("제휴업체")); + return SignUpResponseDTO.from(member, null); } @@ -421,6 +427,8 @@ public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile si .build()); member.setProfile(admin); + runAfterCommit(() -> discordNotifier.sendSignupPendingAlert("학생회")); + return SignUpResponseDTO.from(member, null); } @@ -495,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/report/service/ReportServiceImpl.java b/src/main/java/com/assu/server/domain/report/service/ReportServiceImpl.java index 38f818d3..08cc6771 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::sendReportAlert); + return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); } @@ -112,9 +118,24 @@ public ReportResponseDTO.CreateReportResponse reportStudent(Long reporterId, savedReport.getTargetId(), savedReport.getStatus())); + runAfterCommit(discordNotifier::sendReportAlert); + 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/infra/discord/DiscordNotifier.java b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java index 11f671de..e3fef3fb 100644 --- a/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java +++ b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java @@ -25,6 +25,14 @@ public void sendInquiryAlert() { send("📬 새 문의가 접수되었습니다. 백오피스에서 확인해 주세요."); } + public void sendReportAlert() { + send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요."); + } + + public void sendSignupPendingAlert(String role) { + send("🔔 새 " + role + " 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요."); + } + private void send(String message) { if (serviceAlertWebhookUrl.isBlank()) { return; From 3f1ad465350acaf58922ee04475c3e2f39bd2cbe Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Sun, 4 Oct 2026 17:37:23 +0900 Subject: [PATCH 6/9] =?UTF-8?q?[FEAT/#487]=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EC=99=84=EB=A3=8C=20Discord=20=EC=95=8C=EB=A6=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(=EB=AC=B8=EC=9D=98=20=EB=8B=B5=EB=B3=80=C2=B7?= =?UTF-8?q?=EC=8B=A0=EA=B3=A0=C2=B7=ED=9A=8C=EC=9B=90=EA=B0=80=EC=9E=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - DiscordNotifier에 처리 완료 알림 메서드 6종 추가 - BackofficeInquiryServiceImpl: 문의 답변 완료 시 Discord 알림 - BackofficeReportServiceImpl: 신고 상태 변경·콘텐츠 삭제·기각 시 Discord 알림 - BackofficeMemberServiceImpl: 제휴업체/학생회 가입 승인·거절 시 Discord 알림 - 모두 afterCommit 패턴 적용 --- .../service/BackofficeMemberServiceImpl.java | 24 +++++++++++++++++ .../service/BackofficeReportServiceImpl.java | 27 +++++++++++++++++++ .../service/BackofficeInquiryServiceImpl.java | 18 +++++++++++++ .../server/infra/discord/DiscordNotifier.java | 24 +++++++++++++++++ 4 files changed, 93 insertions(+) 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 c3bb8d90..b2993a1a 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.sendMemberApprovedAlert(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.sendMemberRejectedAlert(roleLabel)); + return BackofficeMemberSummaryDTO.from(member); } @@ -302,6 +313,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..93ae7232 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.sendReportStatusChangedAlert(statusLabel)); + return BackofficeReportResponseDTO.from(report); } @@ -81,6 +92,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteReview(Long reviewId) } processRelatedReports(ReportTargetType.REVIEW, reviewId); + runAfterCommit(discordNotifier::sendReportContentDeletedAlert); return BackofficeReportDTO.SoftDeleteResponseDTO.of(reviewId); } @@ -95,6 +107,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteSuggestion(Long sugge } processRelatedReports(ReportTargetType.SUGGESTION, suggestionId); + runAfterCommit(discordNotifier::sendReportContentDeletedAlert); 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::sendReportRejectedAlert); 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/inquiry/service/BackofficeInquiryServiceImpl.java b/src/main/java/com/assu/server/domain/inquiry/service/BackofficeInquiryServiceImpl.java index 9188aecf..4356a9a3 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::sendInquiryAnsweredAlert); + } + + 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/infra/discord/DiscordNotifier.java b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java index e3fef3fb..7168f6aa 100644 --- a/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java +++ b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java @@ -33,6 +33,30 @@ public void sendSignupPendingAlert(String role) { send("🔔 새 " + role + " 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요."); } + public void sendInquiryAnsweredAlert() { + send("💬 문의 답변이 등록되었습니다."); + } + + public void sendReportStatusChangedAlert(String statusLabel) { + send("📋 신고 상태가 변경되었습니다: " + statusLabel); + } + + public void sendReportContentDeletedAlert() { + send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다."); + } + + public void sendReportRejectedAlert() { + send("✅ 신고가 기각되었습니다."); + } + + public void sendMemberApprovedAlert(String role) { + send("✅ " + role + " 회원가입이 승인되었습니다."); + } + + public void sendMemberRejectedAlert(String role) { + send("❌ " + role + " 회원가입이 거절되었습니다."); + } + private void send(String message) { if (serviceAlertWebhookUrl.isBlank()) { return; From 493bcd01a6f3f7a37556c8a08118b3367efd77cb Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:04:08 +0900 Subject: [PATCH 7/9] =?UTF-8?q?[FIX/#487]=20SignUpServiceImplTest=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=EC=9E=90=EC=97=90=20DiscordNotifier=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/domain/auth/service/SignUpServiceImplTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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 From 9e50aae9194696a822a3714c1d829d4c2edce7f4 Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:23:28 +0900 Subject: [PATCH 8/9] =?UTF-8?q?[FIX/#487]=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EB=88=84=EB=9D=BD=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(DiscordNotifier,=20MeterRegistry)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/DeviceTokenServiceImplTest.java | 6 ++++++ .../service/BackofficeInquiryServiceImplTest.java | 4 ++++ .../domain/inquiry/service/InquiryServiceImplTest.java | 10 ++++++++++ .../domain/report/service/ReportServiceImplTest.java | 4 ++++ 4 files changed, 24 insertions(+) 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; From 55ed7383b4ed9ad0e811100fed05906ab25073cf Mon Sep 17 00:00:00 2001 From: MiN <81948815+leesumin0526@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:09:37 +0900 Subject: [PATCH 9/9] =?UTF-8?q?[REFACTOR/#487]=20DiscordNotifier=20?= =?UTF-8?q?=EC=95=8C=EB=A6=BC=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=A5=BC=20sen?= =?UTF-8?q?d(message)=20=EB=8B=A8=EC=9D=BC=20=EB=A9=94=EC=84=9C=EB=93=9C?= =?UTF-8?q?=EB=A1=9C=20=ED=86=B5=ED=95=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 알림 종류별 메서드 제거 후 send(String)을 public으로 변경 - 각 서비스에서 알림 메시지를 직접 전달하도록 수정 --- .../auth/service/SignUpServiceImpl.java | 4 +- .../service/BackofficeMemberServiceImpl.java | 4 +- .../service/BackofficeReportServiceImpl.java | 8 ++-- .../service/BackofficeInquiryServiceImpl.java | 2 +- .../inquiry/service/InquiryServiceImpl.java | 2 +- .../report/service/ReportServiceImpl.java | 4 +- .../server/infra/discord/DiscordNotifier.java | 38 +------------------ 7 files changed, 13 insertions(+), 49 deletions(-) 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 a6e8d4f1..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 @@ -241,7 +241,7 @@ public SignUpResponseDTO signupPartner(PartnerSignUpRequestDTO req, MultipartFil linkStore(partner, info.name(), address, info.detailAddress(), lat, lng, point); - runAfterCommit(() -> discordNotifier.sendSignupPendingAlert("제휴업체")); + runAfterCommit(() -> discordNotifier.send("🔔 새 제휴업체 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요.")); return SignUpResponseDTO.from(member, null); } @@ -427,7 +427,7 @@ public SignUpResponseDTO signupAdmin(AdminSignUpRequestDTO req, MultipartFile si .build()); member.setProfile(admin); - runAfterCommit(() -> discordNotifier.sendSignupPendingAlert("학생회")); + runAfterCommit(() -> discordNotifier.send("🔔 새 학생회 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요.")); return SignUpResponseDTO.from(member, null); } 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 b2993a1a..aff2e9ec 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 @@ -98,7 +98,7 @@ public BackofficeMemberSummaryDTO approveMember(Long memberId) { } String roleLabel = member.getRole() == UserRole.PARTNER ? "제휴업체" : "학생회"; - runAfterCommit(() -> discordNotifier.sendMemberApprovedAlert(roleLabel)); + runAfterCommit(() -> discordNotifier.send("✅ " + roleLabel + " 회원가입이 승인되었습니다.")); return BackofficeMemberSummaryDTO.from(member); } @@ -112,7 +112,7 @@ public BackofficeMemberSummaryDTO rejectMember(Long memberId) { member.setIsActivated(ActivationStatus.INACTIVE); String roleLabel = member.getRole() == UserRole.PARTNER ? "제휴업체" : "학생회"; - runAfterCommit(() -> discordNotifier.sendMemberRejectedAlert(roleLabel)); + runAfterCommit(() -> discordNotifier.send("❌ " + roleLabel + " 회원가입이 거절되었습니다.")); return BackofficeMemberSummaryDTO.from(member); } 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 93ae7232..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 @@ -77,7 +77,7 @@ public BackofficeReportResponseDTO updateReportStatus(Long reportId, BackofficeR case PROCESSED -> "처리 완료"; case REJECTED -> "기각"; }; - runAfterCommit(() -> discordNotifier.sendReportStatusChangedAlert(statusLabel)); + runAfterCommit(() -> discordNotifier.send("📋 신고 상태가 변경되었습니다: " + statusLabel)); return BackofficeReportResponseDTO.from(report); } @@ -92,7 +92,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteReview(Long reviewId) } processRelatedReports(ReportTargetType.REVIEW, reviewId); - runAfterCommit(discordNotifier::sendReportContentDeletedAlert); + runAfterCommit(() -> discordNotifier.send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다.")); return BackofficeReportDTO.SoftDeleteResponseDTO.of(reviewId); } @@ -107,7 +107,7 @@ public BackofficeReportDTO.SoftDeleteResponseDTO softDeleteSuggestion(Long sugge } processRelatedReports(ReportTargetType.SUGGESTION, suggestionId); - runAfterCommit(discordNotifier::sendReportContentDeletedAlert); + runAfterCommit(() -> discordNotifier.send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다.")); return BackofficeReportDTO.SoftDeleteResponseDTO.of(suggestionId); } @@ -124,7 +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::sendReportRejectedAlert); + runAfterCommit(() -> discordNotifier.send("✅ 신고가 기각되었습니다.")); return BackofficeReportDTO.RejectReportResponseDTO.of(reportId); } 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 4356a9a3..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 @@ -71,7 +71,7 @@ public void answer(Long inquiryId, String answerText) { } inquiry.answer(answerText); - runAfterCommit(discordNotifier::sendInquiryAnsweredAlert); + runAfterCommit(() -> discordNotifier.send("💬 문의 답변이 등록되었습니다.")); } private void runAfterCommit(Runnable task) { 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 dde06618..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 @@ -42,7 +42,7 @@ public Long create(InquiryCreateRequestDTO inquiryCreateRequestDTO, Long memberI inquiryRepository.save(inquiry); incrementAfterCommit(meterRegistry.counter("inquiry.created")); - runAfterCommit(discordNotifier::sendInquiryAlert); + runAfterCommit(() -> discordNotifier.send("📬 새 문의가 접수되었습니다. 백오피스에서 확인해 주세요.")); return inquiry.getId(); } 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 08cc6771..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 @@ -73,7 +73,7 @@ public ReportResponseDTO.CreateReportResponse reportContent(Long reporterId, savedReport.getTargetId(), savedReport.getStatus())); - runAfterCommit(discordNotifier::sendReportAlert); + runAfterCommit(() -> discordNotifier.send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요.")); return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); } @@ -118,7 +118,7 @@ public ReportResponseDTO.CreateReportResponse reportStudent(Long reporterId, savedReport.getTargetId(), savedReport.getStatus())); - runAfterCommit(discordNotifier::sendReportAlert); + runAfterCommit(() -> discordNotifier.send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요.")); return ReportResponseDTO.CreateReportResponse.of(savedReport.getId()); } diff --git a/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java index 7168f6aa..d3f00152 100644 --- a/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java +++ b/src/main/java/com/assu/server/infra/discord/DiscordNotifier.java @@ -21,43 +21,7 @@ public DiscordNotifier(RestClient.Builder builder) { this.restClient = builder.build(); } - public void sendInquiryAlert() { - send("📬 새 문의가 접수되었습니다. 백오피스에서 확인해 주세요."); - } - - public void sendReportAlert() { - send("🚨 새 신고가 접수되었습니다. 백오피스에서 확인해 주세요."); - } - - public void sendSignupPendingAlert(String role) { - send("🔔 새 " + role + " 가입 승인 요청이 있습니다. 백오피스에서 확인해 주세요."); - } - - public void sendInquiryAnsweredAlert() { - send("💬 문의 답변이 등록되었습니다."); - } - - public void sendReportStatusChangedAlert(String statusLabel) { - send("📋 신고 상태가 변경되었습니다: " + statusLabel); - } - - public void sendReportContentDeletedAlert() { - send("🗑️ 신고된 콘텐츠가 삭제 처리되었습니다."); - } - - public void sendReportRejectedAlert() { - send("✅ 신고가 기각되었습니다."); - } - - public void sendMemberApprovedAlert(String role) { - send("✅ " + role + " 회원가입이 승인되었습니다."); - } - - public void sendMemberRejectedAlert(String role) { - send("❌ " + role + " 회원가입이 거절되었습니다."); - } - - private void send(String message) { + public void send(String message) { if (serviceAlertWebhookUrl.isBlank()) { return; }