21 분 소요

서버 환경

  • oracle db 서버
  • 우분투 22.04.2 LTS 웹 nginx 서버
  • tomcat WAS

Like 기능 ( 좋아요 기능 ) 만들기

설명

  • 사용자가 게시글의 좋아요 버튼을 누르면 좋아요 테이블에 사용자 sid 와 게시글 sid 가 같이 저장된다.
    Pasted image 20230603233541
  • 게시글을 삭제하기전 likes 테이블에서 게시글과 관련된 데이터를 먼저 삭제해줘야 한다. 바로 게시글 삭제를 시도하면 무결성을 위배되었다는 오류가 발생한다.
    Pasted image 20230603155100

DB likes 테이블

만들기

  • 좋아요 정보를 저장하기 위한 테이블을 만들어 준다.
    Pasted image 20230603233549
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

  • 클릭하면 좋아요가 해제 또는 선택된다.
    Pasted image 20230603182159
    Pasted image 20230603182036
  • 좋아요 버튼을 누르면 db의 좋아요 테이블에 저장된다.
    Pasted image 20230603233443
  • 좋아요 를 취소하면 좋아요 테이블에서 삭제된다.
    Pasted image 20230603233503

notice.jsp

  • 하트 모양의 좋아요 버튼을 만들었다.
  • class 명을 바꾸면 하트색이 바뀐다.
  • 좋아요 수를 볼 수 있다.
    Pasted image 20230604001643
<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 형태로 받게된다.
    Pasted image 20230603235651
  • ‘true’ 를 응답으로 받으면 현재 사용자는 좋아요를 클릭한 상태이다.
    Pasted image 20230603182036
  • ‘false’ 를 응답으로 받으면 현재 사용자는 좋아요를 안 클릭한 상태이다.
    Pasted image 20230603182159
<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

  • 좋아요 수를 게시글 목록에 표시해 준다.
    Pasted image 20230604001438
<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();
        }
    }
  • 게시글을 삭제할 때 좋아요 테이블에서 게시글과 관련된 정보들을 먼저 삭제해야한다. 먼저 삭제하지 않으면 무결성 위배 오류가 발생한다.
    Pasted image 20230603155100
    // 게시글 삭제할 때 좋아요 테이블에서 게시글과 관련된 데이터 모두 지우기.
    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){

좋아요 결과

  • 게시글 삭제 전 좋아요 테이블
    Pasted image 20230603231538
    Pasted image 20230603231643
    Pasted image 20230603231659
  • 게시글 삭제 후 좋아요 테이블
    Pasted image 20230603231720
  • 좋아요 클릭
    Pasted image 20230604010909
    Pasted image 20230604011015
  • 좋아요 해제
    Pasted image 20230604011030
    Pasted image 20230604011041
  • 여러 사람이 좋아요 클릭했을 경우
    Pasted image 20230604011418

게시글 정렬

설명

  • 조회수 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를 이용하여 묶어준다.
    Pasted image 20230604143109
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 항목으로 정렬을 한다. 좋아요 수가 같으면 게시글 생성일 순으로 정렬을 한다.
    Pasted image 20230604143648
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 파라미터에 넣어 전송한다.
    Pasted image 20230604165442
    ?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");
            
            ... 생략 ...

결과

  • 날짜순 으로 정렬하기.
    Pasted image 20230604164430
  • 제목순 으로 정렬하기.
    Pasted image 20230604164519
  • 작성자명순으로 정렬하기.
    Pasted image 20230604164547
  • 좋아요 수로 정렬하기.
    Pasted image 20230604164629
  • 조회수로 정렬하기.
    Pasted image 20230604165515

후기

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

참고 사이트

댓글남기기