서버 환경
- oracle db 서버
- 우분투 22.04.2 LTS 웹 nginx 서버
- tomcat WAS
Like 기능 ( 좋아요 기능 ) 만들기
설명
- 사용자가 게시글의
좋아요
버튼을 누르면 좋아요
테이블에 사용자 sid 와 게시글 sid 가 같이 저장된다.

- 게시글을 삭제하기전 likes 테이블에서 게시글과 관련된 데이터를 먼저 삭제해줘야 한다. 바로 게시글 삭제를 시도하면 무결성을 위배되었다는 오류가 발생한다.

DB likes
테이블
만들기
좋아요
정보를 저장하기 위한 테이블을 만들어 준다.

create table likes(
user_sid NUMBER(20),
notice_board_sid NUMBER(20),
foreign key (user_sid) references users (sid),
foreign key (notice_board_sid) references notice_board (sid),
primary key (user_sid, notice_board_sid)
);
데이터 갯수
select count(*) from likes where notice_board_sid=yy;
사용자 좋아요 여부 확인
- 사용자가
좋아요
눌렀는지 안눌렀는지 확인한다.
select count(*) from likes where user_sid=xx and notice_board_sid=yy;
데이터 입력
insert into likes (user_sid, notice_board_sid) values (xx, yy);
데이터 삭제
likes
테이블 데이터 부터 삭제 안하면 무결성 위배 오류가 발생한다.
-- 사용자가 좋아요 취소할 때 or 사용자 계정이 없어질 때
delete from likes where user_sid=xx;
-- 게시글 삭제할 때
delete from likes where notice_board_sid=yy;
-- 사용자 좋아요 취소
delete from likes where user_sid=xx and notice_board_sid=yy;
좋아요 버튼 JSP
- 클릭하면
좋아요
가 해제 또는 선택된다.


좋아요
버튼을 누르면 db의 좋아요
테이블에 저장된다.

좋아요
를 취소하면 좋아요
테이블에서 삭제된다.

notice.jsp
- 하트 모양의
좋아요
버튼을 만들었다.
- class 명을 바꾸면 하트색이 바뀐다.
좋아요
수를 볼 수 있다.

<style>
.heart {
width: 15px;
height: 15px;
background: #ea2027;
position: relative;
transform: rotate(45deg);
}
.heart::before,.heart::after {
content: "";
width: 15px;
height: 15px;
position: absolute;
border-radius: 50%;
background: #ea2027;
}
.heart::before {
left: -50%;
}
.heart::after {
top: -50%;
}
.heart-no {
width: 15px;
height: 15px;
background: #e0e0e0;
position: relative;
transform: rotate(45deg);
}
.heart-no::before,.heart-no::after {
content: "";
width: 15px;
height: 15px;
position: absolute;
border-radius: 50%;
background: #e0e0e0;
}
.heart-no::before {
left: -50%;
}
.heart-no::after {
top: -50%;
}
</style>
<div>
<div id="heart" style="float: left;" class="heart-no" onclick="heartClick()"></div>
<span style="margin-left: 10px;">0</span>
</div>
좋아요
버튼이 클릭되면 heartClick()
스크립트 함수로 넘어간다. 그리고 이 함수에서 서버로 좋아요
클릭 또는 해제 상태와 게시글 id를 파라미터에 넣어 서버로 POST 요청을 하게 된다.
- fetch 함수를 이용하여 POST 요청을 보낸다.
- 서버의 응답으로는 현재 사용자의
좋아요
상태와 현재까지 눌린 좋아요
수를 json 형태로 받게된다.

- ‘true’ 를 응답으로 받으면 현재 사용자는
좋아요
를 클릭한 상태이다.

- ‘false’ 를 응답으로 받으면 현재 사용자는
좋아요
를 안 클릭한 상태이다.

<script>
var heart = false; // TODO
fetch("likes", {
method:"POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: postPrameter("confirm")
}).then(res => res.json()).then(json => jsonFunc(json));
function heartClick(){
if(heart){
// 좋아요 취소
// 서버에 좋아요 취소 요청 보내기.
fetch("likes", {
method:"POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: postPrameter("false")
}).then(res => res.json()).then(json => jsonFunc(json));
}else{
// 좋아요!
// 서버에 좋아요 요청 보내기.
fetch("likes", {
method:"POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded"
},
body: postPrameter("true")
}).then(res => res.json()).then(json => jsonFunc(json));
}
}
function postPrameter(s){
var re = "like="+s+"&pageid=<%=notice.getSid() %>";
return re;
}
function jsonFunc(json){
// console.log(json.result);
// console.log(json.likecount);
heart = json.result == "true";
if(heart){
document.getElementById('heart').className = 'heart';
}else{
document.getElementById('heart').className = 'heart-no';
}
document.getElementById('like-count').innerHTML = json.likecount;
}
</script>
notice_board.jsp
좋아요
수를 게시글 목록에 표시해 준다.

<span style="color:#922024;"><%=n.getLikes() %> likes</span><br>
좋아요 버튼 Servlet
MainPage.java
좋아요
요청이 오면 servlet에서 좋아요
를 db 테이블에 추가하거나 삭제한다.
- JSON 형태로 응답을 한다.
- 응답 데이터에는 현재 사용자의
좋아요
여부와 게시글의 좋아요
수가 포함된다.
public class MainPage extends HttpServlet{
private boolean uriSearch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
... 생략 ...
var noticeBoardLikes = "/main_page/notice_board/likes";
// 좋아요 설정 및 해제
... 생략 ...
if(request.getMethod().equals("POST") && uri.equals(noticeBoardLikes)){
var re = true;
long cnt = 0;
// true : 좋아요 한 다음 갯수 알아보기.
// false : 좋아요 취소한 다음 갯수 알아보기.
// confirm : 오직 좋아요 갯수만 알아보기.
var method = request.getParameter("like");
var noticeId = request.getParameter("pageid");
// like 검사, false 좋아요 해제
if(!(method.equals("true") || method.equals("false") || method.equals("confirm"))){
re = false;
cnt = 0;
simplePage(response, "{\"result\":\""+re+"\", \"likecount\":\""+cnt+"\"}");
return true;
}
// 존재하는 게시글인지 확인하기 아니면 fail! false 좋아요 해제
long noticeSid = 0;
try{
noticeSid = Long.parseLong(noticeId);
var noticeDAO = new NoticeBoardDAO(getServletContext());
var notice = noticeDAO.getNotice(noticeSid);
if(notice == null){
re = false;
cnt = 0;
simplePage(response, "{\"result\":\""+re+"\", \"likecount\":\""+cnt+"\"}");
return true;
}
}catch(Exception e){
e.printStackTrace();
}
// 사용자 정보 가지고 오기
var user = (User)request.getAttribute("user");
var noticeLikeDAO = new NoticeLikeDAO(getServletContext());
// 1. method 에 따른 계산 시작! 좋아요 증가 or 좋아요 삭제 등등
if(method.equals("true")){ // 좋아요 추가
noticeLikeDAO.addLike(user.getSid(), noticeSid);
}else if(method.equals("false")) { // 좋아요 삭제
noticeLikeDAO.deleteLike(user.getSid(), noticeSid);
} //method 가 confirm 일 때는 pass
// 2. 사용자 좋아요 싫어요 여부 확인
var isLikeUser = noticeLikeDAO.isUserLike(user.getSid(), noticeSid);
re = isLikeUser;
// 3. 게시글 좋아요 갯수 카운트
cnt = noticeLikeDAO.getCount(noticeSid);
System.out.println("좋아용 "+method+" "+noticeId+" like count : "+cnt);
simplePage(response, "{\"result\":\""+re+"\", \"likecount\":\""+cnt+"\"}");
return true;
}
NoticeLikeDAO.java
likes
테이블에 쉽게 접속하기위한 클래스이다.
public class NoticeLikeDAO {
private String tableName = "likes";
private Connection conn;
private DBConnection dbConn;
public NoticeLikeDAO(ServletContext context){
dbConn = new DBConnection(context, "noticeBoard");
}
... 생략 ...
private void Connection(){
conn = dbConn.connectDB();
}
private void Close(){
try{
conn.close();
}catch(Exception e){
System.out.println(e.getMessage());
}
}
}
public long getCount(long noticeSid){
long likes = 0;
var query = new StringBuilder();
query.append("select count(*) from ");
query.append(tableName);
query.append(" where notice_board_sid=?");
try{
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, noticeSid);
var result = pstmt.executeQuery();
if(result.next()){
likes = result.getLong(1);
}
}catch(Exception e){
e.printStackTrace();
likes = 0;
}finally{
Close();
}
return likes;
}
- 사용자 좋아요를 클릭했는지 안했는지를 판단한다.
- true 이면 현재 사용자는 클릭한 상태이다.
- false 이면 현재 사용자는 클릭을 안한 상태이다.
// 사용자 좋아요 했는지 안했는지 판단한다.
public boolean isUserLike(long userSid,long noticeSid){
long cnt = 0;
var query = new StringBuilder();
query.append("select count(*) from ");
query.append(tableName);
query.append(" where user_sid=? and notice_board_sid=?");
try{
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, userSid);
pstmt.setLong(2, noticeSid);
var result = pstmt.executeQuery();
if(result.next()){
cnt = result.getLong(1);
}
}catch(Exception e){
e.printStackTrace();
cnt = 0;
}finally {
Close();
}
return cnt == 1;
}
- 테이블에
좋아요
클릭 여부를 추가하거나 삭제한다.
// 사용자 게시글 좋아요 추가하기.
public void addLike(long userSid, long noticeSid){
var query = new StringBuilder();
query.append("insert into ");
query.append(tableName);
query.append(" (user_sid, notice_board_sid) values (?, ?)");
try{
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, userSid);
pstmt.setLong(2, noticeSid);
pstmt.executeUpdate();
}catch(Exception e){
e.printStackTrace();
}finally {
Close();
}
}
// 사용자 게시글 좋아요 삭제하기.
public void deleteLike(long userSid,long noticeSid){
var query = new StringBuilder();
query.append("delete from ");
query.append(tableName);
query.append(" where user_sid=? and notice_board_sid=?");
try {
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, userSid);
pstmt.setLong(2, noticeSid);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally{
Close();
}
}
- 게시글을 삭제할 때
좋아요
테이블에서 게시글과 관련된 정보들을 먼저 삭제해야한다. 먼저 삭제하지 않으면 무결성 위배 오류가 발생한다.

// 게시글 삭제할 때 좋아요 테이블에서 게시글과 관련된 데이터 모두 지우기.
public void deleteNoticeLike(long noticeSid){
var query = new StringBuilder();
query.append("delete from ");
query.append(tableName);
query.append(" where notice_board_sid=?");
try {
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, noticeSid);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally{
Close();
}
}
- 마찬가지로 사용자 계정을 삭제할 때도 먼저
좋아요
테이블에서 사용자와 관련된 정보를 삭제해야한다. 먼저 삭제안하면 무결성 위배 오류가 발생한다.
// 사용자가 탈퇴할 때 좋아요 테이블에서 사용자와 관련된 데이터 모두 지우기.
public void deleteUserLike(long userSid){
var query = new StringBuilder();
query.append("delete from ");
query.append(tableName);
query.append(" where user_sid=?");
try {
Connection();
var pstmt = conn.prepareStatement(query.toString());
pstmt.setLong(1, userSid);
pstmt.executeUpdate();
} catch (Exception e) {
e.printStackTrace();
}finally{
Close();
}
}
Notice.java
좋아요
수를 저장하기 위한 변수를 만들어준다.
좋아요
수 변수를 읽기 위한 함수역시 만들어 준다.
public class Notice {
... 생략 ...
private long likes;
public Notice(
long sid,
long userSID,
String username,
long genTime,
String title,
String mainText,
long views,
long likes
){
... 생략 ...
this.likes = likes;
}
... 생략 ...
public long getLikes(){
return likes;
}
NoticeBoardDAO.java
- 게시글을 삭제할 때 먼저
좋아요
테이블에서 게시글과 관련된 정보를 삭제한다.
public class NoticeBoardDAO {
... 생략 ...
// 삭제
public boolean deleteNotice(long sid){
// 좋아요 테이블에서 게시글과 관련된 좋아요 기록 모두 삭제
new NoticeLikeDAO(context).deleteNoticeLike(sid);
}
- 게시글들을 읽어올 때나 게시글 하나를 읽어올 때
좋아요
수를 구해서 게시글 클래스에 저장하여준다.
public Notice getNotice(long sid){
... 생략 ...
try{
... 생략 ...
if(result.next()){
var noticeSid = result.getLong(1);
long likes = new NoticeLikeDAO(context).getCount(noticeSid);
notice = new Notice(
noticeSid,
result.getLong(2),
result.getString(3),
result.getLong(4),
result.getString(5),
result.getString(6),
result.getLong(7),
likes
);
}
}... 생략 ...
}
public ArrayList<Notice> getNoticeList(int optionVal,String question, int start, int number, long from, long to){
... 생략 ...
try{
... 생략 ...
while(result.next()){
var noticeSid = result.getLong(1);
long likes = new NoticeLikeDAO(context).getCount(noticeSid);
var mainText = result.getString(6);
var notice = new Notice(
noticeSid,
result.getLong(2),
result.getString(3),
result.getLong(4),
result.getString(5),
mainText.substring(0, (mainText.length() < maxLen)?mainText.length():maxLen),
result.getLong(7),
likes
);
arr.add(notice);
}
}catch(Exception e){
좋아요
결과
- 게시글 삭제 전
좋아요
테이블



- 게시글 삭제 후 좋아요 테이블

좋아요
클릭


좋아요
해제


- 여러 사람이
좋아요
클릭했을 경우

게시글 정렬
설명
- 조회수 or 날짜 or 제목 or 작성자명 or 좋아요수 에 따라 게시글들을 정렬한다.
- 서버에서 사용자의 정렬 요청에 따라 페이지를 만들어 사용자에게 응답해준다.
DB 정렬 SQL 질의문
조회수 정렬
select * from notice_board order by views desc, gen_time desc;
날짜 정렬
select * from notice_board order by gen_time desc;
제목 정렬
- 제목을 우선으로 정렬을 하지만 같은 제목이 등장하면 생성일을 이용하여 정렬을 한다.
select * from notice_board order by title,gen_time desc;
작성자명 정렬
- 작성자명을 기준으로 먼저 정렬을 하고 같은 작성자명을 지닌 게시글은 생성일을 이용하여 정렬한다.
select * from notice_board order by username, gen_time desc;
좋아요수 정렬
likes
테이블을 게시글의 sid로 group by를 이용하여 묶어준다.

select notice_board_sid,count(notice_board_sid) as cnt
from likes
group by notice_board_sid;
notice_board
테이블과 group by 한 likes
테이블을 게시글 sid를 이용하여 left join을 해준다.
좋아요
수가 null 값으로 나어면 coalesce
함수를 이용하여 0으로 바꿔준다.
좋아요
수를 나타내는 count 항목으로 정렬을 한다. 좋아요
수가 같으면 게시글 생성일 순으로 정렬을 한다.

select sid, user_sid, username, gen_time,title,main_text,views,coalesce(b.cnt,0) as count from notice_board left join (select likes.notice_board_sid,count(likes.user_sid) as cnt from likes group by likes.notice_board_sid) b on sid = notice_board_sid
order by count desc, gen_time desc;
select sid, user_sid, username, gen_time,title,main_text,views,coalesce(b.cnt,0) as count from notice_board left join (select notice_board_sid,count(notice_board_sid) as cnt from likes group by notice_board_sid) b on sid = notice_board_sid
where username like '%test%'
order by count desc, gen_time desc
OFFSET 0 ROWS FETCH NEXT 5 ROWS ONLY;
JSP
notice_board.jsp
- 검색을 서버에 요청할 때 어떤 순으로 정렬할지
order by
파라미터에 넣어 전송한다.

?option_val=1&page=1&q=&date_from=&date_to=&order_by=2
order_by
값으로 0 or 1 or 2 or 3 or 4 값을 넣어 서버로 요청을 보낸다. 만약 틀린 값을 넣어 보내면 날짜 순으로 정렬하게 된다.
- servlet 으로 부터 받은 orderBy 값을 이용하여 default 로 설정된 정렬 옵션을 선택한다.
<form action="" id="searchForm">
<label>검색</label>
<input type="text" style="display:none;" name="page" value="1">
<input type="text" name="q" value="">
<input type="date" name="date_from">
<input type="date" name="date_to">
<input type="submit" value="검색">
<br>
<input type="radio" name="order_by" value="0" <% if(orderBy == 0) { %> checked <% } %> >조회수
<input type="radio" name="order_by" value="1" <% if(orderBy == 1) { %> checked <% } %> >날짜
<input type="radio" name="order_by" value="2" <% if(orderBy == 2) { %> checked <% } %> >제목
<input type="radio" name="order_by" value="3" <% if(orderBy == 3) { %> checked <% } %> >작성자명
<input type="radio" name="order_by" value="4" <% if(orderBy == 4) { %> checked <% } %> >좋아요수
</form>
Servlet
MainPage.java
- 사용자 요청으로 정렬방법을
order_by
파라미터로 받는다.
oreder_by
파라미터는 오직 0,1,2,3,4 값만 가질 수 있다.
- 정렬 순서 값을 이용하여 게시글 검색 데이터를 가지고 온다.
public class MainPage extends HttpServlet{
private boolean uriSearch(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException{
... 생략 ...
// 게시판
if(request.getMethod().equals("GET") && uri.equals(noticeBoard)){
// *** 게시글 정렬 방법 ****
// 0. 조회수 순
// 1. 날짜순
// 2. 제목 순
// 3. 작성자 순
// 4. 좋아요 순
var orderBy = 1;
try{
orderBy = Integer.parseInt(request.getParameter("order_by"));
}catch(Exception e){
e.printStackTrace();
orderBy = 1;
}
if(orderBy < 0 || orderBy > 4){
orderBy = 1;
}
request.setAttribute("orderBy", orderBy);
System.out.println("정렬 방법 : "+orderBy);
// *** 게시글 정렬 방법 END ****
... 생략 ...
// 검색 데이터 가져오기.
var array = noticeBoardDAO.getNoticeList(optionVal,q,start,number, from, to, orderBy);
... 생략 ...
NoticeBoardDAO.java
- 게시글 테이블에서 정렬 방법에 맞추어 SQL 쿼리문을 완성한다.
- 정렬 방법에 따른 완성된 SQL 쿼리문을 이용하였다.
public class NoticeBoardDAO {
public ArrayList<Notice> getNoticeList(int optionVal,String question, int start, int number, long from, long to, int orderBy){
... 생략 ...
if(orderBy > 4 || orderBy < 0){
orderBy = 1;
}
... 생략 ...
try{
Connection();
var query = new StringBuilder();
// 정렬 방법 : 4
if(orderBy == 4){
query.append("select sid, user_sid, username, gen_time,title,main_text,views,coalesce(b.cnt,0) as count from ");
query.append(tableName);
query.append(" left join ");
query.append("(select notice_board_sid,count(notice_board_sid) as cnt from ");
query.append(likeTableName);
query.append(" group by notice_board_sid) b on sid = notice_board_sid");
}else{
// 정렬 방법 : 0,1,2,3
query.append("select * from ");
query.append(tableName);
}
if(optionVal == 1){
query.append(" where (title like '%'||?||'%' or main_text like '%'||?||'%')");
}else if(optionVal == 2){
query.append(" where (username like '%'||?||'%')");
}else if(optionVal == 3){
query.append(" where (title like '%'||?||'%')");
}else if(optionVal == 4){
query.append(" where (main_text like '%'||?||'%')");
}
// 게시글 범위
query.append(" and (gen_time > ? and gen_time < ?)");
if(orderBy == 0){
// 0. 조회수 정렬
query.append(" order by views desc, gen_time desc ");
}else if(orderBy == 1){
// 1. 생성일 순
query.append(" order by gen_time desc ");
}else if(orderBy == 2){
// 2. 제목 순
query.append(" order by title,gen_time desc ");
}else if(orderBy == 3){
// 3. 작성자 순
query.append(" order by username, gen_time desc ");
}else if(orderBy == 4){
// 4. 좋아요 순
query.append(" order by count desc, gen_time desc ");
}
// 0번째 부터 5개의 자료만 출력해라.
query.append("OFFSET ? ROWS FETCH NEXT ? ROWS ONLY");
... 생략 ...
결과
- 날짜순 으로 정렬하기.

- 제목순 으로 정렬하기.

- 작성자명순으로 정렬하기.

좋아요
수로 정렬하기.

- 조회수로 정렬하기.

후기
- 직접 좋아요 기능을 구현해 보니 db 테이블을 어떻게 활용해야하는지 알 수 있는 좋은 계기가 되었다. 외래키(foregin)를 사용하여 테이블을 만들면 외래키로 사용된 데이터가 있는 테이블 데이터를 삭제할 때 외래키를 이용하여 만든 테이블 부터 삭제해야되는 것을 직접 경험해 볼 수 있었다. 그렇지않으면 무결성 위배 오류가 발생한다. 그리고 게시글 정렬 기능을 만들 때도
left join
이 어떻게 동작되는지 직접 확인해 볼 수 있었다. 여러모로 좋아요 기능과 정렬 기능 만드는 것은 좋은 경험이 된 것 같다.
참고 사이트
댓글남기기