From a60a455bec0d983260e4803939e74b86f8c379e6 Mon Sep 17 00:00:00 2001 From: seoyeonson Date: Fri, 7 Mar 2025 11:21:29 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat=20:=20=EC=95=8C=EB=A6=BC=20=EC=9D=BD?= =?UTF-8?q?=EC=9D=8C=20=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84=20(#18)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../noti/controller/ApiV1NotiController.java | 20 +++++++ .../noti/noti/repository/NotiRepository.java | 2 +- .../domain/noti/noti/service/NotiService.java | 12 ++++ .../global/scheduler/NotiTestScheduler.java | 25 ++++---- .../websocket/NotiWebSocketHandler.java | 59 ++++++++++++++----- .../global/websocket/WebSocketConfig.java | 2 + 6 files changed, 90 insertions(+), 30 deletions(-) diff --git a/src/main/java/cmf/commitField/domain/noti/noti/controller/ApiV1NotiController.java b/src/main/java/cmf/commitField/domain/noti/noti/controller/ApiV1NotiController.java index 3c354fa..ceee671 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/controller/ApiV1NotiController.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/controller/ApiV1NotiController.java @@ -7,6 +7,7 @@ import cmf.commitField.global.error.ErrorCode; import cmf.commitField.global.exception.CustomException; import cmf.commitField.global.globalDto.GlobalResponse; +import cmf.commitField.global.websocket.NotiWebSocketHandler; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.security.core.Authentication; @@ -28,6 +29,7 @@ public class ApiV1NotiController { private final NotiService notiService; private final UserRepository userRepository; + private final NotiWebSocketHandler notiWebSocketHandler; @GetMapping("") public GlobalResponse> getNoti() { @@ -39,6 +41,9 @@ public GlobalResponse> getNoti() { String username = (String) attributes.get("login"); // GitHub ID User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); List notis = notiService.getNotReadNoti(user); + // 웹소켓으로 알림 전송 + notiWebSocketHandler.sendNotification(user, notis); + return GlobalResponse.success(notis); } @@ -49,4 +54,19 @@ public GlobalResponse> getNoti() { public void createNoti() { } + + @PostMapping("/read") + public GlobalResponse readNoti() { + Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); + + if (authentication instanceof OAuth2AuthenticationToken) { + OAuth2User principal = (OAuth2User) authentication.getPrincipal(); + Map attributes = principal.getAttributes(); + String username = (String) attributes.get("login"); // GitHub ID + User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); + notiService.read(user); + return GlobalResponse.success("알림을 읽음 처리했습니다."); + } + return GlobalResponse.error(ErrorCode.LOGIN_REQUIRED); + } } diff --git a/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java index 60cc5d1..f67e472 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java @@ -17,5 +17,5 @@ public interface NotiRepository extends JpaRepository { @Query("SELECT new cmf.commitField.domain.noti.noti.dto.NotiDto(n.id, n.message, n.createdAt) " + "FROM Noti n JOIN n.receiver u WHERE u.id = :receiverId AND n.isRead = :isRead") Optional> findNotiDtoByReceiverId(@Param("receiverId") Long receiverId, @Param("isRead") boolean isRead); - + Optional> findNotiByReceiver(User receiver); } diff --git a/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java index 1ca991d..5950a17 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java @@ -88,4 +88,16 @@ public void createNewSeason(Season season) { }); System.out.println("새 시즌 알림 생성 끝"); } + + // 읽음 처리 + @Transactional + public List read(User receiver) { + System.out.println("알림 읽음 처리"); + List notis = notiRepository.findNotiByReceiver(receiver).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); + notis.forEach(noti -> { + noti.setRead(true); + }); + System.out.println("알림 읽음 처리 끝"); + return notis; + } } diff --git a/src/main/java/cmf/commitField/global/scheduler/NotiTestScheduler.java b/src/main/java/cmf/commitField/global/scheduler/NotiTestScheduler.java index 190e779..ce6a60e 100644 --- a/src/main/java/cmf/commitField/global/scheduler/NotiTestScheduler.java +++ b/src/main/java/cmf/commitField/global/scheduler/NotiTestScheduler.java @@ -1,8 +1,12 @@ package cmf.commitField.global.scheduler; import cmf.commitField.domain.noti.noti.service.NotiService; +import cmf.commitField.domain.user.entity.User; import cmf.commitField.domain.user.repository.UserRepository; +import cmf.commitField.global.error.ErrorCode; +import cmf.commitField.global.exception.CustomException; import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component @@ -11,18 +15,11 @@ public class NotiTestScheduler { private final NotiService notiService; private final UserRepository userRepository; -// @Scheduled(cron = "0 44 * * * *") -// public void test() { -// System.out.println("test 실행"); -// Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); -// -// if (authentication instanceof OAuth2AuthenticationToken) { -// OAuth2User principal = (OAuth2User) authentication.getPrincipal(); -// Map attributes = principal.getAttributes(); -// String username = (String) attributes.get("login"); // GitHub ID -// User user = userRepository.findByUsername(username).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); -// notiService.createNoti(user); -// } -// -// } + @Scheduled(cron = "30 14 * * * *") + public void test() { + System.out.println("test 실행"); + + User user = userRepository.findById(1L).orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND_USER)); + notiService.createNoti(user); + } } diff --git a/src/main/java/cmf/commitField/global/websocket/NotiWebSocketHandler.java b/src/main/java/cmf/commitField/global/websocket/NotiWebSocketHandler.java index 1ce57f9..fa29660 100644 --- a/src/main/java/cmf/commitField/global/websocket/NotiWebSocketHandler.java +++ b/src/main/java/cmf/commitField/global/websocket/NotiWebSocketHandler.java @@ -1,39 +1,67 @@ package cmf.commitField.global.websocket; +import cmf.commitField.domain.noti.noti.dto.NotiDto; +import cmf.commitField.domain.noti.noti.entity.Noti; +import cmf.commitField.domain.noti.noti.service.NotiService; +import cmf.commitField.domain.user.entity.User; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; import org.springframework.web.socket.*; import java.io.IOException; -import java.util.ArrayList; +import java.time.LocalDateTime; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; @Component +@RequiredArgsConstructor @Slf4j public class NotiWebSocketHandler implements WebSocketHandler { - - private final List sessions = new ArrayList<>(); + private final NotiService notiService; + private final ObjectMapper objectMapper; + private final Map sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { - sessions.add(session); - log.info("알림 WebSocket 연결됨: " + session); + log.info("클라이언트 접속: {}", session.getId()); + + // 연결 성공 메시지 전송 + Map connectMessage = new HashMap<>(); + connectMessage.put("type", "SYSTEM"); + connectMessage.put("connect", "알림 서버에 연결되었습니다."); + connectMessage.put("timestamp", LocalDateTime.now().toString()); + + try { + session.sendMessage(new TextMessage(objectMapper.writeValueAsString(connectMessage))); + } catch (Exception e) { + log.error("연결 메시지 전송 실패: {}", e.getMessage()); + } } @Override public void handleMessage(WebSocketSession session, WebSocketMessage message) throws Exception { - // 알림 메시지 처리 로직 (필요 시 구현) + if (message instanceof TextMessage) { + String payload = ((TextMessage) message).getPayload(); + log.info("Received message: {}", payload); + } else { + log.warn("Received unsupported message type: {}", message.getClass().getSimpleName()); + } } @Override public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception { - log.error("알림 WebSocket 오류: " + exception.getMessage()); + log.error("WebSocket error: ", exception); + session.close(CloseStatus.SERVER_ERROR); } @Override - public void afterConnectionClosed(WebSocketSession session, CloseStatus closeStatus) throws Exception { - sessions.remove(session); - log.info("알림 WebSocket 연결 종료됨: " + session); + public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { + sessions.values().remove(session); + log.info("WebSocket disconnected: {}", status); } @Override @@ -41,13 +69,14 @@ public boolean supportsPartialMessages() { return false; } - // 모든 유저에게 알림 메시지 전송 - public void sendNotificationToAllUsers(String message) { - for (WebSocketSession session : sessions) { + public void sendNotification(User receiver, List noti) { + WebSocketSession session = sessions.get(receiver.getId()); + if (session != null && session.isOpen()) { try { - session.sendMessage(new TextMessage(message)); + String payload = objectMapper.writeValueAsString(noti); + session.sendMessage(new TextMessage(payload)); } catch (IOException e) { - log.error("알림 메시지 전송 실패: " + e.getMessage()); + log.error("Failed to send WebSocket notification", e); } } } diff --git a/src/main/java/cmf/commitField/global/websocket/WebSocketConfig.java b/src/main/java/cmf/commitField/global/websocket/WebSocketConfig.java index b2ff398..fe4c01d 100644 --- a/src/main/java/cmf/commitField/global/websocket/WebSocketConfig.java +++ b/src/main/java/cmf/commitField/global/websocket/WebSocketConfig.java @@ -12,9 +12,11 @@ public class WebSocketConfig implements WebSocketConfigurer { private final ChatWebSocketHandler chatWebSocketHandler; + private final NotiWebSocketHandler notiWebSocketHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(chatWebSocketHandler, "/chat").setAllowedOrigins("*"); + registry.addHandler(notiWebSocketHandler, "/notifications").setAllowedOrigins("*"); } } From 6d55568ca49bacc1f7aae78f22930c826e592cb4 Mon Sep 17 00:00:00 2001 From: seoyeonson Date: Tue, 11 Mar 2025 17:23:47 +0900 Subject: [PATCH 2/2] =?UTF-8?q?feat=20:=20=EC=97=B0=EC=86=8D=20=EC=BB=A4?= =?UTF-8?q?=EB=B0=8B=20=EC=B6=95=ED=95=98=20=EC=95=8C=EB=A6=BC=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#81)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commit/scheduler/CommitScheduler.java | 11 ++++ .../noti/noti/entity/NotiDetailType.java | 41 ++++++++++++--- .../noti/entity/NotiMessageTemplates.java | 20 ------- .../noti/noti/repository/NotiRepository.java | 2 + .../noti/service/CommitSteakNotiService.java | 52 +++++++++++++++++++ .../domain/noti/noti/service/NotiService.java | 14 +---- 6 files changed, 99 insertions(+), 41 deletions(-) delete mode 100644 src/main/java/cmf/commitField/domain/noti/noti/entity/NotiMessageTemplates.java diff --git a/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java b/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java index f4739f5..2368e68 100644 --- a/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java +++ b/src/main/java/cmf/commitField/domain/commit/scheduler/CommitScheduler.java @@ -1,6 +1,11 @@ package cmf.commitField.domain.commit.scheduler; import cmf.commitField.domain.commit.totalCommit.service.TotalCommitService; +import cmf.commitField.domain.noti.noti.entity.NotiDetailType; +import cmf.commitField.domain.noti.noti.entity.NotiType; +import cmf.commitField.domain.noti.noti.event.NotiEvent; +import cmf.commitField.domain.noti.noti.service.CommitSteakNotiService; +import cmf.commitField.domain.noti.noti.service.NotiService; import cmf.commitField.domain.user.entity.User; import cmf.commitField.domain.user.repository.UserRepository; import lombok.RequiredArgsConstructor; @@ -25,6 +30,8 @@ public class CommitScheduler { private final StringRedisTemplate redisTemplate; private final AtomicInteger counter = new AtomicInteger(0); private final SimpMessagingTemplate messagingTemplate; + private final NotiService notiService; + private final CommitSteakNotiService commitSteakNotiService; private final ApplicationEventPublisher eventPublisher; @@ -86,6 +93,7 @@ private void processUserCommit(String username) { updateTotalCommit = totalCommitService.getTotalCommitCount( username ).getTotalCommitContributions(); + int currentStreakCommit = totalCommitService.getTotalCommitCount(username).getCurrentStreakDays(); newCommitCount = updateTotalCommit - currentCommit; // 새로 추가된 커밋 수 @@ -101,6 +109,9 @@ private void processUserCommit(String username) { CommitUpdateEvent event = new CommitUpdateEvent(this, username, newCommitCount); eventPublisher.publishEvent(event); // 이벤트 발생 + + commitSteakNotiService.checkAndCreateSteakNoti(user, currentStreakCommit); + System.out.println("CommitCreatedEvent published for user: " + username); } else if(newCommitCount < 0) { // newCommitCount에 문제가 있을 경우 문제 상황 / 데이터 동기화 필요. db 갱신. diff --git a/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiDetailType.java b/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiDetailType.java index 0137266..04461c5 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiDetailType.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiDetailType.java @@ -1,14 +1,39 @@ package cmf.commitField.domain.noti.noti.entity; +import java.text.MessageFormat; +import java.util.Arrays; + public enum NotiDetailType { - // 업적 - RANK_UP, // 랭킹 상승 - ACHIEVEMENT_COMPLETED, // 업적 달성 + ACHIEVEMENT_COMPLETED("🎉 {0}님이 [{1}] 업적을 달성했습니다!", new String[]{"nickname", "achievementName"}), + STREAK_CONTINUED("🔥 {0}님의 연속 커밋이 {1}일째 이어지고 있습니다!", new String[]{"nickname", "days"}), + STREAK_BROKEN("😢 {0}님의 연속 커밋 기록이 끊겼습니다.", new String[]{"nickname"}), + SEASON_START("🚀 새로운 [{0}] 시즌이 시작되었습니다! 랭킹 경쟁을 준비하세요!", new String[]{"seasonName"}), + RANK_UP("📈 축하합니다! {0}님의 랭킹이 {1}(으)로 상승했습니다! 🎊", new String[]{"nickname", "tier"}), + NOTICE_CREATED("📢 공지사항이 있습니다: {0}", new String[]{"noticeTitle"}); + + private final String template; + private final String[] paramNames; + + NotiDetailType(String template, String[] paramNames) { + this.template = template; + this.paramNames = paramNames; + } - // 연속 - STREAK_CONTINUED, // 연속 커밋 이어짐 - STREAK_BROKEN, // 연속 커밋 끊김 + public String getTemplate() { + return template; + } - NOTICE_CREATED, // 시즌 - SEASON_START // 시즌 시작 + public String[] getParamNames() { + return paramNames; + } + + public String formatMessage(Object... params) { + if (paramNames.length != params.length) { + throw new IllegalArgumentException("🚨 잘못된 파라미터 개수! 필요: " + + Arrays.toString(paramNames) + ", 제공됨: " + Arrays.toString(params)); + } + return MessageFormat.format(template, params); + } } + + diff --git a/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiMessageTemplates.java b/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiMessageTemplates.java deleted file mode 100644 index e3650bf..0000000 --- a/src/main/java/cmf/commitField/domain/noti/noti/entity/NotiMessageTemplates.java +++ /dev/null @@ -1,20 +0,0 @@ -package cmf.commitField.domain.noti.noti.entity; - -import java.util.Map; - -public class NotiMessageTemplates { - // 알림 메시지 템플릿을 저장하는 맵 - private static final Map TEMPLATES = Map.of( - NotiDetailType.ACHIEVEMENT_COMPLETED, "🎉 {0}님이 [{1}] 업적을 달성했습니다!", - NotiDetailType.STREAK_CONTINUED, "🔥 {0}님의 연속 커밋이 {1}일째 이어지고 있습니다!", - NotiDetailType.STREAK_BROKEN, "😢 {0}님의 연속 커밋 기록이 끊겼습니다. 다음번엔 더 오래 유지해봐요!", - NotiDetailType.SEASON_START, "🚀 새로운 [{0}] 시즌 이 시작되었습니다! 랭킹 경쟁을 준비하세요!", - NotiDetailType.RANK_UP, "📈 축하합니다! {0}님의 랭킹이 {1}(으)로 상승했습니다! 🎊", - NotiDetailType.NOTICE_CREATED, "📢 공지사항이 있습니다: {0}" - ); - - // 알림 메시지 템플릿을 반환하는 메서드 - public static String getTemplate(NotiDetailType type) { - return TEMPLATES.getOrDefault(type, "알림 메시지가 없습니다."); - } -} diff --git a/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java index 33102da..83f87f4 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/repository/NotiRepository.java @@ -24,4 +24,6 @@ public interface NotiRepository extends JpaRepository { // 최근 10일 내 동일한 커밋 부재 알림이 있는지 확인 boolean existsByReceiverAndTypeCodeAndType2CodeAndCreatedAtAfter(User receiver, NotiType type, NotiDetailType detailType, LocalDateTime after); + + boolean existsByReceiverAndType2CodeAndCreatedAtAfter(User receiver, NotiDetailType notiDetailType, LocalDateTime todayStart); } \ No newline at end of file diff --git a/src/main/java/cmf/commitField/domain/noti/noti/service/CommitSteakNotiService.java b/src/main/java/cmf/commitField/domain/noti/noti/service/CommitSteakNotiService.java index 828e68c..1a0516b 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/service/CommitSteakNotiService.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/service/CommitSteakNotiService.java @@ -1,13 +1,65 @@ package cmf.commitField.domain.noti.noti.service; +import cmf.commitField.domain.commit.totalCommit.service.TotalCommitService; +import cmf.commitField.domain.noti.noti.entity.NotiDetailType; +import cmf.commitField.domain.noti.noti.entity.NotiType; +import cmf.commitField.domain.noti.noti.repository.NotiRepository; +import cmf.commitField.domain.user.entity.User; +import cmf.commitField.domain.user.repository.UserRepository; +import cmf.commitField.domain.user.service.UserService; +import cmf.commitField.global.error.ErrorCode; +import cmf.commitField.global.exception.CustomException; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; @Service @RequiredArgsConstructor @Slf4j public class CommitSteakNotiService { + private final UserRepository userRepository; + private final TotalCommitService totalCommitService; private final NotiService notiService; + private final NotiRepository notiRepository; + + // 매일 10시 실행 + @Scheduled(cron = "0 0 10 * * *") + @Transactional + public void sendCommitSteakNoti() { + List users = userRepository.findAll(); + + for (User user : users) { + int currentStreakCommit = totalCommitService.getTotalCommitCount(user.getUsername()).getCurrentStreakDays(); + checkAndCreateSteakNoti(user, currentStreakCommit); + } + } + + public void checkAndCreateSteakNoti(User user, int currentStreakCommit) { + boolean alreadyNotified = notiRepository.existsByReceiverAndType2CodeAndCreatedAtAfter( + user, NotiDetailType.STREAK_CONTINUED, LocalDate.now().atStartOfDay() + ); + + log.info("알림 상세 타입: {}", NotiDetailType.STREAK_CONTINUED.name()); + + if (shouldNotify(currentStreakCommit) && !alreadyNotified) { + log.info("🔍 연속 커밋 축하 알림 User: {}, Streak: {}", user.getUsername(), currentStreakCommit); + notiService.createStreakCommitNoti(user, String.valueOf(currentStreakCommit)); + } + } + + /** + * 특정 연속 커밋 횟수에 도달했는지 확인 + */ + public boolean shouldNotify(int streak) { + return streak == 3 || (streak % 10 == 0); + } } diff --git a/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java index 2f636cd..1b42592 100644 --- a/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java +++ b/src/main/java/cmf/commitField/domain/noti/noti/service/NotiService.java @@ -3,14 +3,11 @@ import cmf.commitField.domain.noti.noti.dto.NotiDto; import cmf.commitField.domain.noti.noti.entity.Noti; import cmf.commitField.domain.noti.noti.entity.NotiDetailType; -import cmf.commitField.domain.noti.noti.entity.NotiMessageTemplates; import cmf.commitField.domain.noti.noti.entity.NotiType; import cmf.commitField.domain.noti.noti.event.NotiEvent; import cmf.commitField.domain.noti.noti.repository.NotiRepository; -import cmf.commitField.domain.season.entity.Rank; import cmf.commitField.domain.season.entity.Season; import cmf.commitField.domain.user.entity.User; -import cmf.commitField.domain.user.repository.UserRepository; import cmf.commitField.global.error.ErrorCode; import cmf.commitField.global.exception.CustomException; import lombok.RequiredArgsConstructor; @@ -19,7 +16,6 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; @@ -29,21 +25,13 @@ @Slf4j public class NotiService { private final NotiRepository notiRepository; - private final UserRepository userRepository; private final ApplicationEventPublisher eventPublisher; - // 알림 메시지 생성 - public static String generateMessage(NotiDetailType type, Object... params) { - String template = NotiMessageTemplates.getTemplate(type); - String message = MessageFormat.format(template, params); // params 배열을 그대로 전달 - return message; - } - // 알림 생성 @Transactional public void createNoti(User receiver, NotiType notiType, NotiDetailType notiDetailType, Long relId, String relTypeCode, Object... params) { // 메시지 생성 - String message = NotiService.generateMessage(notiDetailType, params); + String message = notiDetailType.formatMessage(params); // 알림 엔티티 생성 Noti noti = Noti.builder()