알림기능 읽음/안읽음 리펙토링

Sol's·2023년 2월 7일
0

팀프로젝트

목록 보기
22/25

프로젝트를 구현할다가 알림기능에서 읽은 알림과 안읽은 알림을 구분하는것이 어려웠습니다.

읽지 않은 알림은 선명하게 표시하고 읽은알림은 흐리게 표현하고
읽은알림은 읽지 않은 알림뒤에 나오게 처리하여
사용자에게 읽지 않은 알림이 무엇인지 확인 할 수 있게 하기위하여 기능을 구현해 보았습니다.


위의 사진과 같이 읽은알림과 읽지 않은 알림을 구분할 것입니다.

알림 발생

댓글이 작성되면 알림이 발생되는 로직입니다.
처음 알림이 생성되면 readOrNot 필드는 false로 셋팅이 됩니다.

public CommentResponse addComment(CommentRequest commentRequest, Long crewId, String userName) {
        User user = getUser(userName);
        Crew crew = getCrew(crewId);

        Comment comment = commentRepository.save(commentRequest.toEntity(user, crew));
        alarmRepository.save(Alarm.toEntity(user, crew, AlarmType.ADD_COMMENT, comment.getComment()));

        return CommentResponse.of(comment);
    }

Alarm

getReadOrNot(), setReadOrNot()은 알림을 읽었다고 처리하기 위한 로직입니다.

 public class Alarm extends BaseEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    private String fromUserName;
    private Long targetId; 
    private String targetName; //프로필 조회가 userName으로 되어있음
    private String massage;
    private boolean readOrNot;
    private AlarmType alarmType;

    public static Alarm toEntity(User user, Crew crew, AlarmType alarmType, String comment) {
        Alarm alarm = Alarm.builder()
                .user(crew.getUser())
                .alarmType(alarmType)
                .fromUserName(user.getUsername())
                .targetId(crew.getId())
                .massage(comment)
                .build();
        return alarm;
    }
    
    public boolean getReadOrNot() {
        return readOrNot;
    }
    public void setReadOrNot() {
        this.readOrNot = true;
    }
    

AlarmDto

Dto에서 readOrNot을 추가해주었습니다.

@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class AlarmResponse {
    private Long id;
    private AlarmType alarmType;
    private String fromUserName;
    private Long targetId;
    private String text;
    private LocalDateTime createdAt;
    private Boolean readOrNot;

    public static AlarmResponse fromEntity(Alarm alarm) {
        return AlarmResponse.builder()
                .id(alarm.getId())
                .alarmType(AlarmType.LIKE_CREW)
                .fromUserName(alarm.getFromUserName())
                .targetId(alarm.getTargetCrewId())
                .text(alarm.getMassage())
                .createdAt(alarm.getCreatedAt())
                .readOrNot(alarm.getReadOrNot())
                .build();
    }
}

AlaramController

알림 리스트를 보여주고 페이징을 처리하기위한 작업니다.
첫페이지와 마지막페이지 그리고 정렬된 순서를 지정해주었습니다.

@Controller
@RequestMapping("/view/v1")
@RequiredArgsConstructor
@Slf4j
public class AlarmController {

    private final AlarmService alarmService;

    @GetMapping("/alarm")
    public String alarm(Model model, @PageableDefault(page = 0, size = 10) @SortDefault.SortDefaults({
            @SortDefault(sort = "lastModifiedAt", direction = Sort.Direction.DESC),
            @SortDefault(sort = "readOrNot", direction = Sort.Direction.DESC)
    }) Pageable pageable, Authentication authentication){
        Page<AlarmResponse> alarms = alarmService.getAlarms(pageable, authentication.getName());
        int startPage = Math.max(1,alarms.getPageable().getPageNumber() - 4);
        int endPage = Math.min(alarms.getTotalPages(),alarms.getPageable().getPageNumber() + 4);
        model.addAttribute("startPage", startPage);
        model.addAttribute("endPage", endPage);
        model.addAttribute("alarms", alarms);
        return "alarms/alarm";
    }
}

alarm.html

알림 페이지의 html구성입니다.
읽지 않은알림과 읽은 알림을 if문을 통해 처리하였고
알림을 클릭할때 href로 페이지 이동을 처리하기 위해 case문으로 처리하였습니다.

코드가 길어져 핵심 로직만 첨부하였습니다.

    <div class="my-3 p-3 bg-body rounded shadow-sm">
        <h6 class="border-bottom pb-2 mb-0 ">Recent updates</h6>
        <!-- 읽지 않은 알림 -->
        <div  th:if="!${alarm.readOrNot}" class="d-flex text-muted  pt-3" th:each="alarm : ${alarms}">
            <img class="rounded  d-block" style="height: 20px; width: 20px;" th:id="${alarm.getId()}+' imageId'">
           
            <div th:switch="${alarm.alarmType.name}">
                <span th:case="FOLLOW_CREW">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{/view/v1/users/profile/{userName}(userName=${alarm.targetName})}"
                       th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="REVIEW_CREW">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{/view/v1/crews/{user}/reviewList/{targetId}(user=${alarm.username}, targetId=${alarm.targetId})}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="LIKE_COMMENT">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="LIKE_CREW">
                     <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="ADD_COMMENT">
                     <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>

            </div>

        </div>
        <!-- 읽은 알림 -->
        <div  th:if="${alarm.readOrNot}" class="d-flex text-muted opacity-50 pt-3" th:each="alarm : ${alarms}">

            <img class="rounded  d-block" style="height: 20px; width: 20px;" th:id="${alarm.getId()}+' imageId'">

            <div th:switch="${alarm.alarmType.name}">
                <span th:case="FOLLOW_CREW">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{/view/v1/users/profile/{userName}(userName=${alarm.targetName})}"
                       th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="REVIEW_CREW">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{/view/v1/crews/{user}/reviewList/{targetId}(user=${alarm.username}, targetId=${alarm.targetId})}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="LIKE_COMMENT">
                    <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="LIKE_CREW">
                     <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
                <span th:case="ADD_COMMENT">
                     <p class="pb-3 mb-0 small lh-sm border-bottom">
                    <strong class="d-block text-gray-dark" th:text="|${alarm.fromUserName} 님이|">alarm.title</strong>
                    <a th:href="@{'/view/v1/crews/detail/' + *{alarm.targetId}}" th:text="${alarm.text}">alarm.content</a>
                </p>
                </span>
            </div>
        </div>
    </div>

</main>

CrewViewController

알림이 생겼을때 해당 알림을 체크했다면 true로 만들어주는 함수 readAlarms를 작성하였습니다.
crew모집의 알림이 발생했을경우, crew모집페이지에 들어가게 된다면 알림은 true로 읽음처리가 됩니다.

// 크루 게시물 상세 페이지
    @Transactional
    @GetMapping("/{crewId}")
    public String detailCrew(@PathVariable Long crewId, Model model, Authentication authentication) {
        try {
            //알림 체크
            crewService.readAlarms(crewId, authentication.getName());

            CrewDetailResponse details = crewService.detailCrew(crewId);
            int count = likeViewService.getLikeCrew(crewId);

            model.addAttribute("details", details);
            // 좋아요 개수 출력
            model.addAttribute("likeCnt",count);
        } catch (EntityNotFoundException e) {
            return "redirect:/index";
        }
        return "crew/read-crew";
    }

crewService

알림을 체크하는 로직입니다.

@Transactional
    public void readAlarms(Long crewId, String username) {
        User user = userRepository.findByUserName(username).orElseThrow(() -> new AppException(ErrorCode.USERID_NOT_FOUND, ErrorCode.USERID_NOT_FOUND.getMessage()));
        List<Alarm> alarms = user.getAlarms();
        for (Alarm alarm : alarms) {
            boolean readOrNot = alarm.getReadOrNot();
            if (alarm.getTargetCrewId() == crewId && !readOrNot) {
                alarm.setReadOrNot();
                log.info("알람을 읽었습니다 : {}        알림 : {}", alarm.getId(), alarm.getReadOrNot());
            }
        }
    }

느낀점

간단히 읽음처리를 bool타입의 필드를 추가하여 기능을 구현하면 되겠지 라고 판단하여 시작하였지만
프론트 딴에서 로직처리를 어떻게 할지 방법을 몰라 시간이 오래걸렸습니다.

결국 타임리프의 if문과 case를 통하여 기능을 구현하였습니다.

알림을 읽은처리할때 알림을 누르면 읽음처리를 해야할지, 알림이 발생한 곳을 방문하면 읽음처리를 할지 고민이 많았습니다.
왜냐하면 전자의 경우 같은 곳에서 다수의 알림이 발생하게되면 이미 확인한 게시물의 알림의 경우에도 일일이 확인해야하기 떄문에 사용자 입장에서 번거로움이 많이 생기는 점이 고민거리였고

그렇다고 후자의 기능으로 넣으면 게시물에서 발생한 알림이 한번에 읽음처리가 되기에 사용자가 놓치는 알림이 생길 수 있다고 판단하였기 때문입니다.

결국 후자의 로직으로 처리를 했지만, 회사에 들어가게 된다면 충분히 팀원들과 상의해볼 가치가 있다고 생각이 들었습니다.

profile
배우고, 생각하고, 행동해라

0개의 댓글