- 다른 사용자가 게시글에 댓글을 남길 경우 해당 게시글을 작성한 사용자에게 알림이 뜨도록 구현해보았습니다.
- 실시간으로 알림을 보낼 필요는 없어보였기 때문에 인터셉터를 통한 비연결형으로 구현하였습니다.
- 인터셉터에 대한 자세한 설명은 아래 링크에서 확인하실 수 있습니다.
📝 ReplyRepository
- 우선 다음과 같이 SQL 구문을 작성하여 함수를 구현하였습니다.
package com.cos.blog.repository;
...
// @Repository
public interface ReplyRepository extends JpaRepository<Reply, Long>{
@Query(value="select * from reply where board_id in (select id from board where user_id = :user_id) and user_id != :user_id "
+ "order by id desc limit 10", nativeQuery = true)
List<Reply> findReplyNotification(Long user_id);
}
- 간단하게 설명하자면 현재 접속하여 로그인한 사용자가 작성한 모든 게시글에 달린 댓글 중 최대 10개를 불러오는 함수입니다. 참고로 본인의 댓글은 불러오지 않습니다.
📝 AlarmApiController
- 컨트롤러는 아래와 같이 구현하였습니다.
package com.cos.blog.controller.api;
...
@RestController
@RequiredArgsConstructor
public class AlarmApiController {
private final AlarmService alarmService;
@PutMapping("/api/confirm/{reply_id}")
public ResponseDto<Integer> alarmConfirm(@PathVariable Long reply_id) {
alarmService.alarmConfirm(reply_id);
return new ResponseDto<Integer>(HttpStatus.OK.value(), 1);
}
}
📝 AlarmService
- 서비스단은 아래와 같습니다.
package com.cos.blog.service;
...
@Service
@RequiredArgsConstructor
public class AlarmService {
private final ReplyRepository replyRepository;
@Transactional
public void alarmConfirm(Long reply_id) {
Reply reply = replyRepository.findById(reply_id).orElseThrow(() -> {
return new IllegalArgumentException("알람 확인 실패: 댓글 id를 찾을 수 없습니다.");
});
reply.setAlarm_confirm_state(true);
}
}
- 여기서 alarm_confirm_state는 해당 댓글이 알림 창에서 확인된 댓글인지 아닌지를 판단하기 위한 변수로써 View단에서 이를 구분하여 출력하기 위해 추가하였습니다.
📝 Alarm.js
- ajax 요청은 아래와 같이 구현하였습니다.
function alarmConfirm(reply_id, board_id) {
$.ajax({
type: "PUT",
url: `/api/confirm/${reply_id}`
}).done(function(resp) {
location.href = `/board/${board_id}`;
}).fail(function(error) {
console.log(error);
});
}
- 즉, 사용자가 알림 창에서 알림을 클릭하게 되면 해당 알림에 해당하는 게시글로 이동하게 될 것입니다.
📝 header.jsp
- jsp 파일은 아래와 같이 구현해주었습니다.
<c:if test="${fn:length(alarms) > 0}">
<div class="dropdown">
<div style="color: white; position:relative;" class="btn dropdown" data-toggle="dropdown">
<i class="fa-solid fa-bell"></i>
<c:set var="alarm_count" value="0" />
<c:forEach var="alarm" items="${alarms}">
<c:if test="${!alarm.alarm_confirm_state}">
<c:set var="alarm_count" value="${alarm_count + 1}" />
</c:if>
</c:forEach>
<c:if test="${alarm_count > 0}">
<span class="nav-counter">${alarm_count}</span>
</c:if>
</div>
<div class="dropdown-menu dropdown-menu-right alarm-box">
<span class="alarm-new">새소식 </span><span class="alarm-count">${alarm_count}</span>
<c:forEach var="alarm" items="${alarms}">
<div class="dropdown-item alarm" onclick="alarmConfirm(${alarm.id}, ${alarm.board.id})"
<c:if test="${alarm.alarm_confirm_state}">, style="background-color: whiteSmoke;"</c:if>
>
<span style="float: right;">${alarm.createDate}</span>
<span>
<div class="alarm-content"><span class="alarm-username">${alarm.user.nickname}</span><span>님이 댓글을 남겼습니다.</span></div>
<div class="alarm-content">${alarm.content}</div>
<div class="alarm-content alarm-title">${alarm.board.title}</div>
</span>
</div>
</c:forEach>
</div>
</div>
</c:if>
📝 NotificationInterceptor
- 다음으로 위에서 작성한 코드들을 통해 웹 사이트의 모든 페이지에서 알림 창을 보여지도록 하기 위해 인터셉터를 추가해주었습니다.
package com.cos.blog.interceptor;
...
@Component
@RequiredArgsConstructor
public class NotificationInterceptor implements HandlerInterceptor {
private final ReplyRepository replyRepository;
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
ModelAndView modelAndView) throws Exception {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (modelAndView != null && authentication != null && !authentication.getName().equals("anonymousUser")) {
User user = ((PrincipalDetail) authentication.getPrincipal()).getUser();
modelAndView.addObject("alarms", replyRepository.findReplyNotification(user.getId()));
}
}
}
- 해당 메서드는 페이지가 출력되기 전에 항상 실행되어 할당된 작업을 수행할 것입니다.
- 여기서는 현재 로그인한 사용자에 대하여 작업을 수행하고 있습니다.
📝 SecurityConfig
- 마지막으로 위에서 작성한 인터셉터를 설정하기 위해 아래와 같이 코드를 추가해주었습니다.
package com.cos.blog.config;
...
@Configuration //빈등록 (IoC관리)
@EnableWebSecurity //security 필터 등록
@EnableGlobalMethodSecurity(prePostEnabled = true) //특정 주소로 접근하면 권한 및 인증을 미리 체크하겠다는 뜻
@RequiredArgsConstructor
public class SecurityConfig implements WebMvcConfigurer {
private final NotificationInterceptor notificationInterceptor;
...
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(notificationInterceptor)
.excludePathPatterns("/js/**", "/css/**", "/image/**");
}
...
📝 Result
💡 알게 된 점
- 스프링 인터셉터의 개념과 적용 방법
📌 Reference
'🚗 Backend Toy Project > 스프링 부트 게시판' 카테고리의 다른 글
[스프링부트 게시판] JPA Specification을 통해 쿼리 조건 다루기 (0) | 2022.10.06 |
---|---|
[스프링부트 게시판] 34. 사용자 프로필 이미지 추가 (0) | 2022.09.27 |
[스프링부트 게시판] 32. 이전에 봤던 글 표시 (0) | 2022.09.17 |
[스프링부트 게시판] 31. 이전글, 다음글 구현 (0) | 2022.09.06 |
[스프링부트 게시판] 30. 게시판 정렬 기능 구현 (0) | 2022.09.03 |