김미썸코딩

Spring MVC 로 하트(좋아요) 구현하기 - 모달창 포함 본문

빅데이터 플랫폼 구축을 위한 자바 개발자 양성과정/랜선여행 커뮤니티 프로젝트

Spring MVC 로 하트(좋아요) 구현하기 - 모달창 포함

김미썸 2021. 4. 21. 15:50
728x90

사진자랑게시판(게시판명: picture)의 '좋아요'기능을 예로 들었다.

picture 게시판은 카드 리스트가 있는데 카드리스트에서 카드를 누르면 카드의 상세내용 모달창이 뜬다. 

카드 리스트와 모달창에서 하트를 누르는게 모두 가능하도록 구현한다.

 


*** 순서 ***

1. 구현 완료 모습

2. DB 테이블 구축

3. 코드

           3-1. JSP

           3-2. Controller

           3-3. DAO

           3-4. TO

           3-5. SQL(mapper)


1. 구현 완료 모습

위처럼 카드들이 나열된 형태의 게시판이다. 카드를 누르면 해당 카드의 모달창이 뜨도록 구현하였다.

 

여기서 '좋아요'기능은 하트svg 로 구현하였다.

 

아래처럼 리스트에서 하트 클릭이 가능하고..

카드를 클릭하면 나오는 모달창에서도 하트클릭이 가능하다.

 

 

2. DB 테이블 구축

#사진자랑 게시판 좋아요 테이블(p_heart)
create table p_heart(
  hno int auto_increment primary key,
  bno int not null,
  userid varchar(100) not null,
  constraint p_heart_bno_fk foreign key(bno) references p_board(no)
  on delete cascade,
  constraint p_heart_userid_fk foreign key(userid) references user(nick)
  on delete cascade on update cascade
);

 

순번 컬럼명 컬럼설명 데이터 타입 Null여부 제약조건
0 hno 좋아요 번호 int(11)   primary_key
auto_increment
1 bno 좋아요된 게시물 번호 int(11) not null foreign key(bno) references p_board(no)
on delete cascade
2 userid 좋아요 한 사용자 nick varchar(100) not null foreign key(userid) references user(nick)
on delete cascade on update cascade

 

 

컬럼은 세개지만 foreign keybnouserid에 대해 제약조건을 걸었다.

 

1. 게시물(bno) 제약조건 p_heart_bno_fk

  • 게시물이 삭제되면 하트도 사라진다. on delete cascade

2. 사용자(userid) 제약조건 p_heart_userid_fk

  • 사용자가 삭제되면(탈퇴) 하트도 사라진다. on delete cascade
  • 사용자의 닉네임이 수정되면 userid도 수정된다. on update cascade

 

 

3. 코드

3-1 . JSP

> view > picture > ajax_page.jsp

아래처럼 for 문에 모든 코드들이 들어가 있다.

아래 for문으로 페이지의 카드 리스트가 완성된다.

for문 한번 돌때 하나의 카드와 그 카드에 해당하는 모달창이 만들어진다.

<c:forEach var="tmp" items="${list }">

	...
    
	(리스트 페이지의 카드의 하트부분 코드)

	...

	(카드에 해당하는 모달창 하트부분 코드)
    
	...
    
</c:forEach>

 

▶ (리스트 페이지의 카드의 하트부분 코드)

<c:choose>
    <%-- 로그인 상태일때 - 하트 클릭 되게 --%>
    <c:when test="${not empty sessionScope.nick}">
        <c:choose>
            <c:when test="${empty tmp.hno}">
                <%-- 빈 하트일때 --%>
                <span> <a idx="${tmp.no }" href="javascript:"
                    class="heart-click heart_icon${tmp.no }"><svg
                            xmlns="http://www.w3.org/2000/svg" width="16" height="16"
                            fill="currentColor" class="bi bi-suit-heart"
                            viewBox="0 0 16 16">
                                  <path
                                d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" />
                                </svg></a>
                </span>
            </c:when>
            <c:otherwise>
                <%-- 꽉찬 하트일때 --%>
                <span> <a idx="${tmp.no }" href="javascript:"
                    class="heart-click heart_icon${tmp.no }"><svg
                            xmlns="http://www.w3.org/2000/svg" width="16" height="16"
                            fill="currentColor" class="bi bi-suit-heart-fill"
                            viewBox="0 0 16 16">
                                  <path
                                d="M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z" />
                                </svg></a>
                </span>
            </c:otherwise>
        </c:choose>
    </c:when>
    <%-- 로그인 상태가 아닐때  - 하트클릭 안되게 --%>
    <c:otherwise>
        <span> <a href="javascript:" class="heart-notlogin">
                <svg class="heart3" xmlns="http://www.w3.org/2000/svg"
                    width="16" height="16" fill="currentColor"
                    class="bi bi-suit-heart" viewBox="0 0 16 16">
                          <path
                        d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" />
                        </svg>
        </a>
        </span>
    </c:otherwise>
</c:choose>
<span id="heart${tmp.no }">${tmp.heart }</span>

 

▶ (카드에 해당하는 모달창 하트부분 코드)

<c:choose>
    <%-- 로그인 상태일때 - 하트 클릭 되게 --%>
    <c:when test="${not empty sessionScope.nick}">
        <c:choose>
            <c:when test="${empty tmp.hno}">
                <%-- 빈 하트일때 --%>
                <a idx="${tmp.no}" href="javascript:"
                    class="heart-click heart_icon${tmp.no }"><svg
                        xmlns="http://www.w3.org/2000/svg" width="16" height="16"
                        fill="currentColor" class="bi bi-suit-heart"
                        viewBox="0 0 16 16">
                          <path
                            d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" />
                        </svg></a>

            </c:when>
            <c:otherwise>
                <%-- 꽉찬 하트일때 --%>
                <a idx="${tmp.no}" href="javascript:"
                    class="heart-click heart_icon${tmp.no }"><svg
                        xmlns="http://www.w3.org/2000/svg" width="16" height="16"
                        fill="currentColor" class="bi bi-suit-heart-fill"
                        viewBox="0 0 16 16">
                          <path
                            d="M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z" />
                        </svg></a>
            </c:otherwise>
        </c:choose>
    </c:when>
    <%-- 로그인 상태가 아닐때  - 하트클릭 안되게 --%>
    <c:otherwise>
        <a href="javascript:" class="heart-notlogin"> <svg
                class="heart3" xmlns="http://www.w3.org/2000/svg" width="16"
                height="16" fill="currentColor" class="bi bi-suit-heart"
                viewBox="0 0 16 16">
                  <path
                    d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" />
                </svg></a>
    </c:otherwise>
</c:choose>
</span> <span id="m_heart${tmp.no }">${tmp.heart }</span>

하트를 원래 페이지의 리스트와 모달창 중 어디에서 누르든 동시에 반영되게 하기 위해서

하트의 클래스명을 모두 동일하게 맞춰주었다. 

 

▶ 페이지가 열리자마자 실행되는 javascript

$(document).ready(function() {

    GetList(1);
    /* 카드가 나타나는 애니메이션
    $(document).ready(function() {
        $(window).scroll( function(){
            $('.thumb').each( function(i){

                var bottom_of_element = $(this).offset().top + $(this).outerHeight();
                var bottom_of_window = $(window).scrollTop() + $(window).height();

                if( bottom_of_window > bottom_of_element ){
                    $(this).animate({'opacity':'1','margin-bottom':'0px'},1000);
                }

            }); 
        });
    });
    */

    // 게시물 이미지를 클릭했을 때 실행된다
    // 해당 게시물을 hit+1하는 함수를 호출한다.
    $(document).on('click', '.card-img', function() {
        // 게시물 번호(no)를 idx로 전달받아 저장합니다.
        let no = $(this).attr('idx');

        console.log(no +"에 hit + 1을 함");

        // hit+1하고 그 값을 불러온다.
        $.ajax({
            url : 'picture_view.do',
            type : 'get',
            data : {
                no : no
            },
            success : function(to) {
                let hit = to.hit;

                $('#m_hit'+no).text(hit);
                $('#hit'+no).text(hit);

            },
            error : function() {
                alert('서버 에러');
            }
        });
    });


});

// 창 크기가 변할 때마다 가로세로 길이를 맞춰준다.
$(window).resize(function(){
    $('.box').each(function(){
        $(this).height($(this).width());
    });
}).resize();

 

▶ 하트 클릭시 이벤트 처리 javascript

// 로그인 한 상태에서 하트를 클릭했을 때 (로그인한 상태인 하트의 <a></a> class명: heart-click)
$(".heart-click").click(function() {

    // 게시물 번호(no)를 idx로 전달받아 저장합니다.
    let no = $(this).attr('idx');
    console.log("heart-click");

    // 빈하트를 눌렀을때
    if($(this).children('svg').attr('class') == "bi bi-suit-heart"){
        console.log("빈하트 클릭" + no);

        $.ajax({
            url : 'saveHeart.do',
            type : 'get',
            data : {
                no : no,
            },
            success : function(pto) {
                //페이지 새로고침
                //document.location.reload(true);

                let heart = pto.heart;

                // 페이지, 모달창에 하트수 갱신
                $('#m_heart'+no).text(heart);
                $('#heart'+no).text(heart);

                console.log("하트추가 성공");
            },
            error : function() {
                alert('서버 에러');
            }
        });
        console.log("꽉찬하트로 바껴라!");

        // 꽉찬하트로 바꾸기
        $(this).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");
        $('.heart_icon'+no).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");

    // 꽉찬 하트를 눌렀을 때
    }else if($(this).children('svg').attr('class') == "bi bi-suit-heart-fill"){
        console.log("꽉찬하트 클릭" + no);

        $.ajax({
            url : 'removeHeart.do',
            type : 'get',
            data : {
                no : no,
            },
            success : function(pto) {
                //페이지 새로고침
                //document.location.reload(true);

                let heart = pto.heart;
                // 페이지, 모달창에 하트수 갱신
                $('#m_heart'+no).text(heart);
                $('#heart'+no).text(heart);

                console.log("하트삭제 성공");
            },
            error : function() {
                alert('서버 에러');
            }
        });
        console.log("빈하트로 바껴라!");

        // 빈하트로 바꾸기
        $(this).html('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-suit-heart" viewBox="0 0 16 16"><path d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" /></svg>');

        $('.heart_icon'+no).html('<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-suit-heart" viewBox="0 0 16 16"><path d="M8 6.236l-.894-1.789c-.222-.443-.607-1.08-1.152-1.595C5.418 2.345 4.776 2 4 2 2.324 2 1 3.326 1 4.92c0 1.211.554 2.066 1.868 3.37.337.334.721.695 1.146 1.093C5.122 10.423 6.5 11.717 8 13.447c1.5-1.73 2.878-3.024 3.986-4.064.425-.398.81-.76 1.146-1.093C14.446 6.986 15 6.131 15 4.92 15 3.326 13.676 2 12 2c-.777 0-1.418.345-1.954.852-.545.515-.93 1.152-1.152 1.595L8 6.236zm.392 8.292a.513.513 0 0 1-.784 0c-1.601-1.902-3.05-3.262-4.243-4.381C1.3 8.208 0 6.989 0 4.92 0 2.755 1.79 1 4 1c1.6 0 2.719 1.05 3.404 2.008.26.365.458.716.596.992a7.55 7.55 0 0 1 .596-.992C9.281 2.049 10.4 1 12 1c2.21 0 4 1.755 4 3.92 0 2.069-1.3 3.288-3.365 5.227-1.193 1.12-2.642 2.48-4.243 4.38z" /></svg>');
    }



});


// 로그인 안한 상태에서 하트를 클릭하면 로그인해야한다는 알림창이 뜹니다.
// (로그인한 상태인 하트의 <a></a> class명: heart-notlogin)
$(".heart-notlogin").unbind('click');
$(".heart-notlogin ").click(function() {
    alert('로그인 하셔야 하트를 누를수 있습니다!');
});

 

위 코드에서 하트 클릭시 ajax 처리 부분이다.

$.ajax({
    url : 'saveHeart.do',
    type : 'get',
    data : {
        no : no,
    },
    success : function(pto) {
        //페이지 새로고침
        //document.location.reload(true);

        let heart = pto.heart;

        // 페이지, 모달창에 하트수 갱신
        $('#m_heart'+no).text(heart);
        $('#heart'+no).text(heart);

        console.log("하트추가 성공");
    },
    error : function() {
        alert('서버 에러');
    }
});

saveHeart.do를 요청해서 백에서 하트 추가를 구현한 후, 갱신된 하트 수를 pto라는 PictureTO에 담아서 가져온다.

리스트와 모달창의 하트 수를 text()를 사용하여 pto.heart로 바꾼다.

 

하트를 꽉찬 하트로 바꾸는 것ajax 밖에서 이루어진다. 

 

[하트 아이콘 변경을 ajax문 밖에서 하는 이유는?]

여기서 하트 클릭시 이벤트 처리는

클릭한 하트가 꽉찬 하트인가 빈하트인가에 따라 하트 추가/해제 이벤트가 나뉘어진다. 

ajax의 success는 백에서 처리 후 성공일때 실행이된다. 

ajax 가 처리되는 동안 한번 더 하트를 눌러버리면? 문제가 발생한다. 

백에선 하트 추가 중인데 사용자가 꽉찬 하트로 바뀌기 전에 한번더 클릭하면 하트 추가가 한번 더 실행되게 된다. 

 

따라서,

이러한 충돌을 방지하기 위하여 하트 아이콘 변경은 ajax의 success문에 넣지 않고,

클릭하면 바로 바뀌게끔 처리해주어야한다.

html() 을 사용하여 처리한다.

 

하트를 꽉찬하트로 바꾸는 부분이다.

// 꽉찬하트로 바꾸기
$(this).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");
$('.heart_icon'+no).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");

만약 하트가 리스트의 카드에서 눌렸다면 this는 리스트의 하트가 되고, '.heart_icon'+no는 모달창의 하트가 된다.

모달창에서 눌렸다면 this는 모달창의 하트가 되고, '.heart_icon'+no는 리스트의 카드의 하트가 된다. 

 

 

[카드와 모달창 끼리 클래스명이 같은데

굳이 $('.heart_icon'+no).html(...) 만으로 하지 않고 $(this).html(...)를 쓴 이유는? ]

먹지 않더라..... 이유는 모르겠다.

만약 모달창이 없는 경우라면 아래처럼 this만 써서 한줄로만 처리하면 되지만

$(this).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");

모달창이 있는 경우엔 하트 아이콘 클래스명을 리스트와 모달창을 통일해주었기때문에 아래 한줄이면 될줄 알았는데..

$('.heart_icon'+no).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");

안됐다. ㅎ

현재 클릭한것만 변경이 되고 리스트에서 누르면 모달창이, 모달창에서 누르면 리스트에 바뀐 하트가 반영되지 않았다.

 

그래서 아래처럼 두줄을 다써주었더니 성공했다.

$(this).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");
$('.heart_icon'+no).html("<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='currentColor' class='bi bi-suit-heart-fill' viewBox='0 0 16 16'><path d='M4 1c2.21 0 4 1.755 4 3.92C8 2.755 9.79 1 12 1s4 1.755 4 3.92c0 3.263-3.234 4.414-7.608 9.608a.513.513 0 0 1-.784 0C3.234 9.334 0 8.183 0 4.92 0 2.755 1.79 1 4 1z'/></svg>");

내생각엔 특정지어야 했는데 클래스명이 여러개를 가리키고 있어서 가리키는 것이 모호해져서? 안먹었던 것같다. 

id값을 줘서 특정지을 수도 있지만 그럼 더 복잡해지므로.. 여기서 만족하는 걸로ㅎㅎ

 

 

3-2. Controller

> PictureController.java

// 빈하트 클릭시 하트 저장
@ResponseBody
@RequestMapping(value = "/saveHeart.do")
public PictureTO save_heart(@RequestParam String no, HttpSession session) {

    PictureHeartTO to = new PictureHeartTO();

    // 게시물 번호 세팅
    to.setBno(no);

    // 좋아요 누른 사람 nick을 userid로 세팅
    to.setUserid((String) session.getAttribute("nick"));

    // +1된 하트 갯수를 담아오기위함
    PictureTO pto = heartDao.pictureSaveHeart(to);

    return pto;
}

// 꽉찬하트 클릭시 하트 해제
@ResponseBody
@RequestMapping(value = "/removeHeart.do")
public PictureTO remove_heart(@RequestParam String no, HttpSession session) {
    PictureHeartTO to = new PictureHeartTO();

    // 게시물 번호 세팅
    to.setBno(no);

    // 좋아요 누른 사람 nick을 userid로 세팅
    to.setUserid((String) session.getAttribute("nick"));

    // -1된 하트 갯수를 담아오기위함
    PictureTO pto = heartDao.pictureRemoveHeart(to);

    return pto;
}

saveHeart.do는 하트를 눌렀을 때의 요청 처리이고, removeHeart.do는 누른 하트를 해제할 때의 요청 처리이다.

두가지 모두 ajax에 대한 처리이므로 @ResponseBody 라는 annotation을 사용했고,

return 값으로 갱신된 heart수를 반환한다.

 

3-3. DAO

> PictureHeartDAO.java

package com.exam.model1.pictureHeart;


import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import com.exam.model1.picture.PictureTO;



@Repository
public class PictureHeartDAO {

	@Autowired
	private SqlSession sqlSession;
	
	public PictureTO pictureSaveHeart(PictureHeartTO to) {
		// p_board 테이블에 해당 게시물의 heart수를 +1 하기위한 to세팅
		PictureTO pto = new PictureTO();
		pto.setNo(to.getBno());
		
		// 해당 게시물의 heart를 +1 한다.
		sqlSession.update("picture_heart_up", pto);
		
		// p_heart 테이블에 추가 
		int result = sqlSession.insert("picture_heart_save", to);
		
		if (result == 1) {	// p_heart 테이블에 새로운 좋아요 추가가 성공한다면..
			// 갱신된 하트 갯수를 가져옴
			pto = sqlSession.selectOne("picture_heart_count", pto);
		}
		return pto;
	}

	public PictureTO pictureRemoveHeart(PictureHeartTO to) {
		// p_board 테이블에 해당 게시물의 heart수를 -1 하기위한 to세팅
		PictureTO pto = new PictureTO();
		pto.setNo(to.getBno());
		
		// 해당 게시물의 heart를 -1 한다.
		sqlSession.update("picture_heart_down", pto);
		
		// p_heart 테이블에서 삭제
		int result = sqlSession.delete("picture_heart_remove", to);
		
		if (result == 1) {	// p_heart 테이블에 좋아요 삭제가 성공한다면..
			// 갱신된 하트 갯수를 가져옴
			pto = sqlSession.selectOne("picture_heart_count", pto);
		}
		return pto;
	}
}

 

 

3-4. TO

> PictureHeartTO.java

package com.exam.model1.pictureHeart;

public class PictureHeartTO {
	private String hno;
	private String bno;
	private String userid;
	
	public String getHno() {
		return hno;
	}
	public void setHno(String hno) {
		this.hno = hno;
	}
	public String getBno() {
		return bno;
	}
	public void setBno(String bno) {
		this.bno = bno;
	}
	public String getUserid() {
		return userid;
	}
	public void setUserid(String userid) {
		this.userid = userid;
	}
}

> PictureTO.java

public class PictureTO {
	private String no;
	private String subject;
	private String content;
	private String writer;
	private String wdate;
	private String hit;
	private String location;
	private String media;
	private String reply;
	private String heart;
	
	// 현재사용자가 좋아요 누른건지 아닌지
	private String hno;
	// 현재사용자가 즐겨찾기 누른건지 아닌지
	private String fno;
	// 글쓴이 프로필 사진 
	private String profile;
	// 현재 사용자 id
	private String nick;
	
	// 시작 게시물 번호
	private int startRowNum;
	// 끝 게시물 번호
	private int endRowNum;
	// 가져갈 게시물 갯수
	private int rowCount;
	
	
	
	
	
	public int getRowCount() {
		return rowCount;
	}
	public void setRowCount(int rowCount) {
		this.rowCount = rowCount;
	}
	public int getStartRowNum() {
		return startRowNum;
	}
	public void setStartRowNum(int startRowNum) {
		this.startRowNum = startRowNum;
	}
	public int getEndRowNum() {
		return endRowNum;
	}
	public void setEndRowNum(int endRowNum) {
		this.endRowNum = endRowNum;
	}
	public String getNick() {
		return nick;
	}
	public void setNick(String nick) {
		this.nick = nick;
	}
	public String getProfile() {
		return profile;
	}
	public void setProfile(String profile) {
		this.profile = profile;
	}
	public String getHno() {
		return hno;
	}
	public void setHno(String hno) {
		this.hno = hno;
	}
	public String getFno() {
		return fno;
	}
	public void setFno(String fno) {
		this.fno = fno;
	}
	public String getNo() {
		return no;
	}
	public void setNo(String no) {
		this.no = no;
	}
	public String getSubject() {
		return subject;
	}
	public void setSubject(String subject) {
		this.subject = subject;
	}
	public String getContent() {
		return content;
	}
	public void setContent(String content) {
		this.content = content;
	}
	public String getWriter() {
		return writer;
	}
	public void setWriter(String writer) {
		this.writer = writer;
	}
	public String getWdate() {
		return wdate;
	}
	public void setWdate(String wdate) {
		this.wdate = wdate;
	}
	public String getHit() {
		return hit;
	}
	public void setHit(String hit) {
		this.hit = hit;
	}
	public String getLocation() {
		return location;
	}
	public void setLocation(String location) {
		this.location = location;
	}
	public String getMedia() {
		return media;
	}
	public void setMedia(String media) {
		this.media = media;
	}
	public String getReply() {
		return reply;
	}
	public void setReply(String reply) {
		this.reply = reply;
	}
	public String getHeart() {
		return heart;
	}
	public void setHeart(String heart) {
		this.heart = heart;
	}
	

}

 

3-5. SQL(mapper.xml)

<!-- 좋아요 추가 -->
<insert id="picture_heart_save" parameterType="com.exam.model1.pictureHeart.PictureHeartTO">
    insert into p_heart
    values(0, #{bno}, #{userid})
</insert>
<update id="picture_heart_up" parameterType="com.exam.model1.picture.PictureTO">
    update p_board set heart=heart+1 
    where no=#{no}
</update>

<!-- 좋아요 삭제 -->
<delete id="picture_heart_remove" parameterType="com.exam.model1.pictureHeart.PictureHeartTO">
    delete from p_heart
    where bno=#{bno} and userid=#{userid}
</delete>
<update id="picture_heart_down" parameterType="com.exam.model1.picture.PictureTO">
    update p_board set heart=heart-1 
    where no=#{no}
</update>

<!-- 좋아요 추가/삭제시 좋아요 갯수 가져오기 -->
<select id="picture_heart_count" parameterType="com.exam.model1.picture.PictureTO" resultType="com.exam.model1.picture.PictureTO">
    select heart 
    from p_board
    where no=#{no}
</select>

 

 

 

 

 

모달창이며..무한 스크롤이며 동적으로 많은 것을 처리하는 게시판이다보니 하트 구현에 생각보다 시간이 걸렸다ㅎㅎ

생각한 대로 잘 안먹는 시행착오도 있었고.. 그래도 해놓고나니 넘나 뿌듯했다.

인스타그램처럼 하트 클릭할 때 임펄스 애니메이션 주고 싶었는데 까먹었다.

나중에 추가해보도록 해야겠다.

 

 

 

※ ajax로 가져오는 picture_view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<jsp:include page="../include/index.jsp"></jsp:include>

<!-- CSS File -->
<link href="./resources/css/picture_write.css" rel="stylesheet">
<link href="./resources/css/navbar.css" rel="stylesheet">


<script type="text/javascript">
	window.onload = function() {
		
		// '등록하기' 버튼 클릭시 모두 입력되었는지 검사
		document.getElementById('submit1').onclick = function() {
			if (document.wfrm.subject.value.trim() == "") {
				alert('제목을 입력하셔야 합니다.');
				return false;
			}

			if (document.wfrm.location.value.trim() == "(선택 안함)") {
				alert('위치를 입력하셔야 합니다.');
				return false;
			}


			// 파일 업로드 확인 메세지
			if (document.wfrm.media.value.trim() == "") {
				alert('파일을 입력하셔야 합니다.');
				return false;
			} else {
				const extension = document.wfrm.media.value.split('.').pop();
				if (extension != 'png' && extension != 'jpg'
						&& extension != 'gif' && extension != 'mp4'&& extension != 'PNG'&& extension != 'JPG'&& extension != 'GIF'&& extension != 'MP4'&& extension != 'MOV'&& extension != 'mov') {
					alert('이미지나 동영상 파일을 입력하셔야 합니다.');
					return false;
				}
			}
			
			// 웹 에디터(썸머노트) 입력확인
			if (document.wfrm.content.value.trim() == "") {
				alert('내용을 입력하셔야 합니다.');
				return false;
			} 
			
			document.wfrm.submit();
			
			
		};
		
		// 파일을 선택하면 파일명이 뜨도록 함
		var file_input_container = $('.js-input-file');
		if (file_input_container[0]) {
			file_input_container.each(function() {

				var that = $(this);

				var fileInput = that.find(".input-file");
				var info = that.find(".input-file__info");

				fileInput.on("change", function() {

					var fileName;
					fileName = $(this).val();

					if (fileName.substring(3, 11) == 'fakepath') {
						fileName = fileName.substring(12);
					}

					if (fileName == "") {
						info.text("No file chosen");
					} else {
						info.text(fileName);
					}

				})

			});
		}
		
		// Summernote 설정
		var toolbar = [
		    // 글꼴 설정
		    ['fontname', ['fontname']],
		    // 글자 크기 설정
		    ['fontsize', ['fontsize']],
		    // 굵기, 기울임꼴, 밑줄,취소 선, 서식지우기
		    ['style', ['bold', 'italic', 'underline','strikethrough', 'clear']],
		    // 글자색
		    ['color', ['forecolor','color']],
		    // 표만들기
		    ['table', ['table']],
		    // 글머리 기호, 번호매기기, 문단정렬
		    ['para', ['ul', 'ol', 'paragraph']],
		    // 줄간격
		    ['height', ['height']],
		    // 그림첨부, 링크만들기, 동영상첨부
		    //['insert',['picture','link','video']],
		    //['insert',['link']],
		    // 코드보기, 확대해서보기, 도움말
		    ['view', ['codeview','fullscreen', 'help']]
		  ];
		
		let setting = {
				height: 300,                 // 에디터 높이
				  minHeight: null,             // 최소 높이
				  maxHeight: null,             // 최대 높이
				  focus: true,                  // 에디터 로딩후 포커스를 맞출지 여부
				  lang: "ko-KR",					// 한글 설정
				  placeholder: '최대 2048자까지 쓸 수 있습니다'	//placeholder 설정
		}
		$('.summernote').summernote(setting);
		
	};
	

</script>


</head>
<body>


	<!-- 메뉴바 
		 현재페이지 뭔지 param.thisPage에 넣어서 navbar.jsp에  던짐 -->
	<jsp:include page="../include/navbar.jsp">
		<jsp:param value="picture_write" name="thisPage" />
	</jsp:include>

	<br />
	<div class="page-wrapper bg-light p-t-100 p-b-50">
		<div class="wrapper wrapper--w900">
			<div class="card card-6">

				<div class="card-heading">
					<h2 class="title">여행한 곳을 자랑하세요!</h2>
				</div>
				<div class="card-body">
					<form action="./picture_write_ok.do" method="post" name="wfrm" enctype="multipart/form-data">
						
						<c:if test="${not empty sessionScope.nick }">
							<input type="hidden" name="writer" value="${nick}" />
						</c:if>
						
						<div class="form-row">
							<div class="name">제목</div>
							<div class="value">
								<input class="input--style-6" type="text" name="subject">
							</div>
						</div>
						<div class="form-row">
							<div class="name">위치</div>
							<div class="value">
								<label for="location">국가</label> 
								<select id="location" name="location" class="form-select">
									<option>(선택 안함)</option>
									<option>영국</option>
									<option>프랑스</option>
									<option>독일</option>
									<option>이탈리아</option>
									<option>스위스</option>
									<option>그리스</option>
									<option>스페인</option>
									<option>포르투갈</option>
									<option>체코</option>
									<option>헝가리</option>
									<option>오스트리아</option>
									<option>스웨덴</option>
									<option>핀란드</option>
									<option>폴란드</option>
								</select>
							</div>
						</div>
						<div class="form-row">
							<div class="name">파일 업로드</div>
							<div class="value">
								<div class="input-group js-input-file">
									<input class="input-file" type="file" name="media" id="file">
									<label class="label--file" for="file">파일 선택</label> <span
										class="input-file__info">No file chosen</span>
								</div>
								<div class="label--desc">여행 사진 or 동영상을 업로드하세요. 최대파일 크기는
									00MB입니다.</div>
							</div>
						</div>
						
						<div class="form-row">
							<div class="name">내용</div>
							<div class="value">
								<div class="summernote-group">
									<textarea class="summernote" name="content" id="content"></textarea>
								</div>
							</div>
						</div>
					</form>
				</div>
				<div class="card-footer">
					<button class="btn btn--radius-2 btn--blue-2" type="submit"
						id="submit1" >등록하기</button>
				</div>
			</div>
		</div>
	</div>

</body>
</html>
728x90
Comments