일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
- 스프링부트
- NIO
- alter
- Calendar
- dfs
- GC로그수집
- map
- JPA
- scanner
- priority_queue
- javascript
- 리소스모니터링
- spring boot
- html
- date
- CSS
- Union-find
- string
- 힙덤프
- set
- Properties
- union_find
- List
- sql
- BFS
- math
- 큐
- 스택
- Java
- deque
- Today
- Total
매일 조금씩
Spring MVC 로 하트(좋아요) 구현하기 - 모달창 포함 본문
Spring MVC 로 하트(좋아요) 구현하기 - 모달창 포함
mezo 2021. 4. 21. 15:50사진자랑게시판(게시판명: 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 key인 bno와 userid에 대해 제약조건을 걸었다.
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>
'빅데이터 플랫폼 구축을 위한 자바 개발자 양성과정 > 랜선여행 커뮤니티 프로젝트' 카테고리의 다른 글
Spring MVC 로 메세지 기능 구현 (42) | 2021.04.27 |
---|---|
Spring MVC 로 무한 스크롤, 검색 구현하기 (8) | 2021.04.22 |
Spring MVC 로 댓글(답글) 구현하기 (16) | 2021.04.21 |
Web socket을 활용한 실시간 댓글 알람 기능 구현 (0) | 2021.03.30 |
페이지에서 '좋아요' 를 하고 뒤로 가기 한 후 다시 돌아왔을때 처리 (0) | 2021.03.29 |