스프링부트 ajax를 사용한 '좋아요' 비동기 처리

2024. 5. 11. 21:04웹개발

728x90

유튜브를 보면 좋아요 버튼을 눌러주면 버튼만 바뀔 뿐 영상이 멈추거나 페이지가 새로 로드되지 않는다.

이는 비동기 처리, 병렬적으로 태스크를 수행하는 것이다.

 

동기와 비동기의 차이는 동기는 직렬적으로 태스크를 수행한 것으로, 요청 후 응답을 받아야만 작동이 된다.

하지만 비동기는 요청을 보내고 바로 다음 태스크가 진행되는 것이다. 그래서 유튜브 영상이 끊이지 않고도 다른 기능을 수행할 수 있는 것이다.

 

스프링부트에서 ajax를 활용한 좋아요를 구상하기 위해서는 서버측에선 테이블(Entity), 레포시토리(Repository),

서비스(Service), 컨트롤러(Controller)가 필요했다.

 

<좋아요 테이블>

@Entity
@Getter
@Setter
@NoArgsConstructor
@ToString

@Table(name = "likes")
@SequenceGenerator(
        name = "likes_SEQ",
        sequenceName = "likes_SEQ",
        initialValue = 1,
        allocationSize = 1
)
public class LikesEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "likes_SEQ")
    private Long likesid;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "userid")
    private UsersEntity users;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "rid")
    private RecipeEntity recipe;

    //likesid는 생성자가 필요 없어서 @AllArgsConstructor 사용 X
    public LikesEntity(UsersEntity users, RecipeEntity recipe) {
        this.users = users;
        this.recipe = recipe;
    }

}

 

좋아요 테이블에서는 회원 정보(userid)와 좋아요를 적용할 레시피(rid)와 연관관계를 맺었다.

 

<Repository>

public interface LikeRepository extends JpaRepository<LikesEntity, Long> {

    //있는지 없는지 검토
    boolean existsByUsersAndRecipe(UsersEntity users, RecipeEntity recipe);
    }

 

boolean을 사용하여 좋아요가 적용 되었는지, 안 되었는지를 판별했다.

 

<Service>

@Service
@RequiredArgsConstructor
@Transactional
public class LikeService {

    private final RecipeService recipeService;
    private final RecipeRepository recipeRepository;
    private final LikeRepository likeRepository;

    public boolean addLike(int rid, UsersEntity users) {
        RecipeEntity recipe = recipeRepository.findByRid(rid);
        if (!likeRepository.existsByUsersAndRecipe(users, recipe)) {
            //호출되면 board에 있는 count 증가
            recipe.setRgoodcnt(recipe.getRgoodcnt()+1);
            // likeRepository에 userid 값이랑 recipe값 저장해버림
            likeRepository.save(new LikesEntity(users, recipe));
            return true;
        }
        else {
            recipe.setRgoodcnt(recipe.getRgoodcnt()-1);
            likeRepository.deleteByUsersAndRecipe(users, recipe);
            return false;
        }
    }

    public boolean existLike(int rid, UsersEntity users) throws Exception {
        RecipeEntity recipe = recipeRepository.findByRid(rid);
        if (likeRepository.existsByUsersAndRecipe(users, recipe)) {
            return true;        //존재하면 true 값
        } else {
            return false;       //없으면 false 값
        }
    }
}

 

<Controller>

@RestController
@RequiredArgsConstructor
public class LikeController {

    private final LikeService likeService;
    private final UsersService usersService;

    @PostMapping("/api/likes/up/{rid}")
    public boolean addLike( HttpSession session, @PathVariable("rid")@Positive int rid) throws Exception { //@PathVariable("rid")는 Spring MVC에서 경로 변수를 받아들이는 방법  / @Positive는 Bean Validation API의 어노테이션 중 하나입니다. 이 어노테이션은 해당 필드 값이 0보다 큰지를 검사

        //회원 번호을 불러옴
        int userid = (int) session.getAttribute("userId");
        UsersEntity users = usersService.getUserByUserId(userid);

        return likeService.addLike(rid, users);
    }

}

 

※ 이번 프로젝트에서는 세션으로 회원정보를 가져왔지만 이는 보안에 취약점이 많다고 했다. 다음부터는 보안에 신경을 많이 써야 할 것 같다.

 

<html>

<div layout:fragment="content">
			
        ...
            
        <div id="goodAndBook" style="text-align: center; margin-bottom: 20px"> <!-- 좋아요, 북마크 버튼 -->
          <input type="hidden" id="recipeID" th:value="${recipeDTO.getRid()}">
          <!-- 좋아요 버튼 -->
          <button id="likeButton1" th:if="${noLogin != null}" data-bs-toggle="modal" data-bs-target="#plzLogin" style="width: 50px; height: 50px;">
            <i id="likeIcon1" class="fa fa-heart-o" style="font-size: 24px;"></i> <!-- 로그인 안 한 사용자에게는 빈 하트 아이콘 표시 -->
          </button>
          <button id="likeButton" onclick="rgoodAjax()" th:if="${noLogin == null}" style="width: 50px; height: 50px;">
            <i id="likeIcon" th:class="${setLike} ? 'fa fa-heart' : 'fa fa-heart-o'" style="font-size: 24px;"></i>
          </button>
        </div>
        
        ...
	
        <javascript>
            const rgoodAjax = () => {
              const rid = $('#recipeID').val();
              $.ajax({
                data: {"rid" : rid },
                type: "post",
                url: "/api/likes/up/" + rid,
                success: (data, textStatus, xhr) => {
                  // HTTP 응답 상태 코드를 확인하여 성공 여부를 판단
                  if (xhr.status === 200) {
                    // 좋아요 상태에 따라 아이콘 변경
                    const likeIcon = $('#likeIcon');
                    if (data === true) {
                      likeIcon.removeClass('fa-heart-o').addClass('fa-heart');
                    } else {
                      likeIcon.removeClass('fa-heart').addClass('fa-heart-o');
                    }
                  }
                },
                error: (error) => {
                  // 에러 처리
                }
              })
            }
        </javascript>
        
</div>

 

타임리프 레이아웃을 사용하면 자바스크립트는 <div layout:fragment="content"> 영역 안에 작성해야 한다. 그것을 모르고 영역 밖에다 작성하니, 네트워크에서 서버로 전송을 하지를 못하여 3일 밤낮을 구글과 지피티를 뒤져봤으나 해결을 못해 포기할 즈음 겨우 찾아냈다.