본문 바로가기

웹 개발

[13주차] 게시판 구현 #9 - 좋아요 / 좋아요 취소 기능

이제 게시판에 좋아요와 좋아요 취소 기능을 구현해보겠습니다.

 

우선 생각한 대략적인 개발흐름은 이렇습니다.


1 ) 한 회원이 좋아요 누르면 그 회원 db에 특정 게시글에 좋아요 눌렀는지 추가
회원 db에 어느어느 게시글에 좋아요 눌렀는지 배열? 이든 뭐든 일단 데이터로 저장되어야 함

> 이 기록을 like_manager라는 새로운 DB에 할 것입니다.

2 ) 게시글은 얼마나 좋아요가 눌렸는지 카운팅
> 이건 board 게시판에 기록합니다.


3 ) 그리고 작성자가 본인이면 좋아요 안됨 (조건문 사용)

이 3가지 항목을 만족시키려면 어떻게 구조를 짜야할까요?

 

 

 

터미널에다가

desc board;

를 입력해서 각 컬럼의 상태를 봅니다.

 

 

제 DB에는 언젠가 구현할 좋아요 기능을 위해 thumbup 컬럼이 만들어져 있습니다.

int Type에 NO NULL이고 DEFAULT는 0입니다.

 

좋아요 기능 구현을 위해 적절한 아이콘을 찾아봅시다.

 

iconfinder에 들어가 like라고 검색 후 찾아봤습니다.

 

iconfinder

아무래도 제 컬럼 네임이 thumbup이다보니 엄지 척 표시가 좋을 것 같습니다.

 

 

좋아요 누른 모습과 누르지 않은 모습이 명확한 두 아이콘을 골랐습니다.

 

 

다운받은 이미지 파일을 image라는 새로운 폴더를 만들어 그 안에 넣어줍니다.

 

폴더에서 확인하니 잘 넣어졌네요 근데 이름이 어렵습니다.

 

src 속성으로 이름을 추가할 것이므로 쉬운 이름으로 바꿔줍니다.

 

 

 


실패한 코딩

<!-- 좋아요 버튼 -->
<img style="cursor: point; width:30px; height:30px;"
id="myimg" src="이미지 파일 주소"> 좋아요 수 : 0

 

img 태그를 추가해주고 style 속성을 줘서 width와 height를 적절하게 조절합니다.

cursor: point는 img에 커서를 갖다대면 모양이 바뀌어서 사용자에게 편의성을 부여합니다.

(cursor: hand는 먹히지 않습니다.)

getElementById로 객체를 건들이기 위해 id="img"를 부여합니다.

 

img 태그 바깥에는 임시로 좋아요 수 : 0 이라는 통 문자열을 넣어줍니다. (직관성)

 

그리고 하단에 script 태그를 작성해줍니다.

(엥간하면 </body> 살짝 위쪽, 안그러면 코드 읽어내려가면서 순서가 뒤틀려서

javascript 실행이 안됨)

<script>
 const myImg = document.getElementById("myimg")
 let is_liked = false;
 myImg.addEventListener("click", function(){
  if (!is_liked){
   myImg.src = './image/like_thumbup.png';
  } else {
   myImg.src ="./image/black_thumbup.png";
  }
  is_liked = !is_liked;
 });
</script>

myImg라는 변수는 getElementById 함수를 이용해서 id가 myimg인 객체를 건드립니다.

그리고 Bool 변수인 is_liked 를 false로 정의합니다. (현재 좋아요 버튼을 안 눌렸다는 뜻)

myImg에서 addEventListener를 통해 클릭 시에 src 속성을 변경할 것입니다.

if 조건문으로 내부가 현재 참이되면 (is_liked가 false이므로 !is_liked는 현재 True)

클릭 했을 때 좋아요 버튼이 눌려진 아이콘으로 바뀝니다.

else로 그 외의 경우는 원래대로 검정색 엄지로 바뀌게 됩니다. (좋아요 취소 상태)

 

그 후, is_liked 변수를 반대로 뒤집습니다. is_liked = !is_liked;

 

실제로 잘 작동하는지 테스트를 해보면,

클릭하면 색깔있는 엄지로 바뀝니다.

 

잘 작동하는 군요.


이제 실제 DB와 연결해서 구현해보겠습니다.

 

우선 회원가입한 회원들을 관리하는 user 테이블에 좋아요한 게시글의 ID를 기록하는 컬럼을 추가하겠습니다.

 

 

잘 추가되었습니다.

 

like_update.php를 만들어 코드를 추가하겠습니다.

 

like_update.php

<?php
session_start();
$login_id = $_SESSION['id'];
//클릭할 때 number GET으로 가져옴
$bno = $_GET['number'];

$con = mysqli_connect('localhost','root','1234','test');

//POST로 전달받은 게시판 넘버를 저장한다 (SQL Injection 대응을 하면서)
//strip_tags로 태그 제거하고 filter_var로 유효성 검사 수행
$boardlike_id = mysqli_real_escape_string($con,filter_var(strip_tags($bno), FILTER_SANITIZE_SPECIAL_CHARS));

//좋아요 누를 때 해당 회원의 DB에 이 게시글의 넘버가 저장됨
//FIND_IN_SET은 문자 구분 자를 통해 후자의 문자열에서 전자의 문자를 찾는다
$like_query = "SELECT whatlike_board FROM user WHERE username='$login_id' AND (FIND_IN_SET($boardlike_id,whatlike_board) > 0)";
$like_result = $con->query($like_query);

if (mysqli_num_rows($like_result) > 0){
    //좋아요가 눌러진 상태라면 - 1
  mysqli_query($con, "UPDATE user SET whatlike_board = REPLACE(whatlike_board,'$boardlike_id,','') WHERE username = '$login_id'");
  mysqli_query($con, "UPDATE board SET thumbup = thumbup -1 WHERE number = '$boardlike_id'");
} else {
    $concat_query = "UPDATE user SET whatlike_board = CONCAT_WS($boardlike_id,'whatlike_board',',') WHERE username = '$login_id'";
    //좋아요가 안 눌러진 상태라면 + 1
    //CONCAT_WS는 문자열 합치는 함수 , 로 구분하며 2번째 인덱스에 1번째를 추가한다는 뜻
    mysqli_query($con, $concat_query);
    mysqli_query($con, "UPDATE board SET thumbup = thumbup + 1 WHERE number = '$boardlike_id'");
}
$hit_query ="UPDATE board SET hit = hit -1 WHERE number ='".$boardlike_id."'";
mysqli_query($con, $hit_query);

//원래 페이지로 바로 리다이렉트
header("Location: ./read.php?number=$boardlike_id");
?>

 

그 다음은 read.php에서 엄지 img를 좀 더 수정해보겠습니다.

엄지 버튼을 누르면 그게 form 태그에 의해서 like_update.php로 넘어가야 하기 때문이죠

img태그를 사용했던 엄지를 <input type="image"로 변경할 것입니다. 이미지 아이콘은 src 속성, style속성 그대로 사용하되

input만 바뀌는 것입니다.

이렇게 하면 이미지를 form태그의 submit버튼처럼 사용할 수 있습니다.

<!-- 좋아요 버튼 -->
<?php
//게시판 리스트에서 read로 갈때 사용했던 GET과 POST둘 다 사용하는 방법
echo 
'<form action="./like_update.php?number='.$bno.'" method="POST">
<input type="image" style="cursor: pointer; width:30px;height:30px;"
id="myimg" name="board_id" src="./image/black_thumbup.png"> 좋아요 수 : '.$board['thumbup'].'
</form>';
?>

 

 

form의 action 속성에 url다음 ?로 파라미터를 주면 form의 method가 POST방식이더라도 GET 방식으로 전달할 수 있습니다.

이런 구조를 짜면 결국 GET과 POST모두 사용할 수 있는 것입니다.

 

웹에서 테스트를 해보겠습니다.

엄지를 눌러주면,

 

좋아요 수가 잘 올라가네요!

 

 

 

DB에서도 한번 살펴볼까요?

wow 잘 추가가 되었습니다

 

그런데 문제가 생겼네요

엄지를 클릭할 때 like_update.php에서 좋아요를 처리한 뒤 다시 read.php로 리다이렉트 되면서

script 태그 안에 심어놓은 javascript 이벤트핸들러가 무용지물이 된 것입니다.

 

좋아요 수는 올라가지만 엄지의 이미지가 바뀌지 않았습니다.

그리고 원인이 뭔지, 좋아요가 눌러진 상태일 때 -1을 하는 부분이 먹질 않습니다.

 

 

 


성공한 코딩

위 실패한 부분에서는 새로운 DB를 만들지 않고 기존 회원 DB에 whatboard_like라는 컬럼을 추가하여

각 회원이 좋아요한 게시글 넘버를 , 를 기준으로 추가합니다. 35, 36, 37 이런식으로 말이죠

해당 기능 구현은 FIND_IN_SET했습니다. FIND_IN_SET은 문자열에서 ,를 기준으로 원하는 문자를 찾아줍니다. 35, 36, 37 에서 36을 FIND_IN_SET 하면 2를 반환해줍니다. (index 2이라는 의미)

그리고 좋아요를 취소할 때 마다 , 를 기준으로 지워갑니다. 36 번을 취소했다면 35, 37로 바뀔 것입니다.

해당 함수는 CONCAT_WS를 사용했습니다.

그런데 FIND_IN_SET과 CONCAT_WS 함수가 잘 작동하지 않아 싸그리 갈아엎고 새로 코딩했습니다...

(아마 따옴표 짝을 잘못 맞췄거나 싶은데..생각나는 모든 경우의 수를 다해봐도 안 됐음)

 


 

 

like_manager 라는 새로운 DB를 생성해주겠습니다.

 

id 와 like_board_id, username을 만들어주고 각각의 설정은 이러합니다.

id는 int, 보기는 unsigned, 그리고 AUTO_INCREMENT 설정을 해줍니다.

like_board_int는 int 타입, username은 var(128)로 설정했습니다.

 

각각 NOT NULL로 했습니다.

 

이제 코드를 살펴보겠습니다.

 

read.php

<!-- 좋아요 버튼 -->
<?php
$login_id = $_SESSION['id'];
//게시판 DB 연결
$con = mysqli_connect('localhost','root','1234','test');

//SQL Injection 대응
$boardlike_id = mysqli_real_escape_string($con,filter_var(strip_tags($bno), FILTER_SANITIZE_SPECIAL_CHARS));
 
 //like_manager DB 검증
 $check_url2 = "SELECT username FROM like_manager WHERE like_board_id=$boardlike_id and username='$login_id'";
 $sql_check2= $con->query($check_url2);
 $res_check2 = $sql_check2->fetch_array();

// 접속한 id와 게시판 작성자가 달라야 좋아요 버튼 생성
if ($_SESSION['id'] != $board['name']){
  //like_manager DB에서 검증 후, 좋아요 기록이 없으면 밋밋한 엄지
  if ($_SESSION['id'] == $res_check2['username']){
   echo
//게시판 리스트에서 read로 갈때 사용했던 GET과 POST둘 다 사용하는 방법 
'<form action="./like_update.php?number='.$boardlike_id.'" method="POST">
<input type="image" style="cursor: pointer; width:30px;height:30px;"
id="myimg" name="board_id" src="./image/thumb2.png"></form>';
  } else if ($_SESSION['id'] != $res_check2['username']){
    //좋아요 기록이 있으면 색깔 엄지
   echo 
'<form action="./like_update.php?number='.$boardlike_id.'" method="POST">
<input type="image" style="cursor: pointer; width:30px;height:30px;"
id="myimg" name="board_id" src="./image/thumb1.png"></form>';
  }
};
echo' 좋아요 수 : '.$board['thumbup'];
?>

위 코드는 read.php의 전체 코드가 아닌 좋아요 기능 관련 코드만을 가져온 것입니다.

 

우선, $login_id = $_SESSION['id']로 정의합니다. 왠진 모르겠지만 $_SESSION 변수를 그냥 사용했을 때 프로그램에서 막히는 경우가 있더라고요 또 편의를 위한 이유도 있습니다.

 

$boardlike_id는 GET방식으로 받아온 해당 게시판 글의 number를 저장하는 $bno라는 변수를

mysqli_real_escape_string 함수로 SQL Injection 대응시킨 변수입니다. 그러므로 $bno를 사용하는 것보단

$boardlike_id를 사용하는 것이 시큐어 코딩의 입장에서도 더 좋겠죠

 

$check_url2는 like 관리를 위해 만들어뒀던 like_manager에 대한 쿼리문입니다.

접속한 회원이 보고 있는 게시글이 좋아요한 게시글인지 아닌지를 판단하는 쿼리입니다.

read.php에서는 username 대조만 하면 되기 때문에 다 불러오지 않고 SELECT username으로 username만 불러왔습니다.

 

본인이 작성한 게시글에는 좋아요 버튼을 누를 수 없도록 세션 아이디 ($_SESSION['id']) 와 작성자 이름 (board['name'])을 비교합니다. 다르면 좋아요 버튼을 보여주지 않고 좋아요 수만 출력합니다. 같다면 좋아요 버튼과 좋아요 수를 모두 출력합니다.

 

그 안에 다른 조건문도 들어있습니다. like_manager DB에 연결해서 접속한 사람의 아이디 ($_SESSION['id'])가 DB 기록에 존재하면 색깔 있는 엄지 (좋아요를 눌렀다는 의미)를 출력하고 DB기록에 없으면 밋밋한 엄지 (좋아요 안 눌렀다는 뜻)를 출력합니다.

 

php 코드 안에 echo로 html 태그를 삽입했습니다. 이러면 조건문 안에 자유롭게 html 코드를 써서 좀 더 넓은 구현을 할 수 있습니다.

데이터 전송은 form 태그를 사용하고 img는 type이 image인 input 태그를 이용했습니다.

나머지 속성은 name이나 id 부여 그리고 크기 조절하는 속성입니다.

방식은 POST방식으로 method를 설정하지만, url 파라미터에 number를 받아서 게시판 리스트를 불러올 때 처럼

GET방식과 POST방식을 모두 사용합니다.

 

 

 

like_update.php

<?php
session_start();
$login_id = $_SESSION['id'];


//클릭할 때 number GET으로 가져옴
$bno = $_GET['number'];

$con = mysqli_connect('localhost','root','1234','test');

//POST로 전달받은 게시판 넘버를 저장한다 (SQL Injection 대응을 하면서)
//strip_tags로 태그 제거하고 filter_var로 유효성 검사 수행
$boardlike_id = mysqli_real_escape_string($con,filter_var(strip_tags($bno), FILTER_SANITIZE_SPECIAL_CHARS));

//window.history.back을 쓰면 뒤로가기 됨
$sql_check = "SELECT * FROM board WHERE number=$boardlike_id";
$res_check = mysqli_fetch_array(mysqli_query($con, $sql_check));
if($login_id == $res_check['name']){
    $hit_query = "UPDATE board SET hit=hit-1 WHERE number=$boardlike_id";
    mysqli_query($con,$hit_query);
    echo "<script>alert('본인의 게시글입니다!');";
    echo "window.history.back()</script>";
    exit;
}

//like_manger DB에 저장
$sql_check2 = "SELECT * FROM like_manager WHERE like_board_id='$boardlike_id' and username='$login_id'";
$res_check2 = mysqli_fetch_array(mysqli_query($con, $sql_check2));
$delete_like_query = "
UPDATE board SET thumbup= thumbup-1 WHERE number=$boardlike_id;
DELETE FROM like_manager WHERE like_board_id=$boardlike_id and username='$login_id';
";

if($res_check2){
  $hit_query = "UPDATE board SET hit=hit-1 WHERE number=$boardlike_id;";
    mysqli_query($con,$hit_query);
    //확인을 누르면 멀티쿼리가 실행되면서 좋아요 수를 1 감소시키고 데이터를 삭제시킴
    echo "<script>if(confirm('이미 좋아한 게시글입니다! 좋아요를 취소하시겠어요?')==true){
      ".mysqli_multi_query($con, $delete_like_query).";
    } else {};
    </script>";
    
    echo "<script>window.history.back()</script>";
    exit;
}

$sql = "
UPDATE board SET thumbup =thumbup +1,hit=hit-1 WHERE number=$boardlike_id;
INSERT INTO like_manager(like_board_id, username) VALUES ($boardlike_id, '$login_id');
";
$res = mysqli_multi_query($con, $sql);
echo "<script>alert('좋아요를 눌렀습니다!');";
echo "window.history.back()</script>";
?>

read.php에서 url로 받은 name이 number인 파라미터를 $bno에 저장합니다.

 

그리고 좋아요 검증하는 like_update.php에서는 여러가지 쿼리문이 번갈아 사용됩니다.

 

본인의 게시글인지 검증하기 위해서 board 게시판에 저장된 해당 게시글의 작성자 이름을 쿼리로 불러와서

조건문으로 비교합니다.

본인의 게시글이 아니면 pass하고 본인의 게시글이 맞다면 조회수를 1 떨어트리고 alert 창을 띄웁니다.

(왜냐면 window.history.back을 사용하면서 다시 read한 것처럼 인식되어 조회수가 1 증가함)

 

타인의 게시글일 때 좋아요한지 아닌지를 검증하기 위해서 like_manager에 연결하는 쿼리문을 작성했습니다.

like_manager DB는 한 회원이 여러 게시글에 좋아요를 누를 것이므로 username 컬럼은 같은 이름이 여러개 있을 수 있기 때문에 WHERE 조건을 걸 때 username 만으로 검증하면 안 됩니다.

 

$boardlike_id로 해당 게시글의 number를 받고 $login_id로 아이디 값을 받아 두 가지 다 맞았을 경우 SELECT 합니다.

($boardlike_id는 아까 read.php에서 사용했던 SQL Injection 방지된 게시글 number)

$sql_check2 = "SELECT * FROM like_manager WHERE like_board_id='$boardlike_id' and username='$login_id'";

해당 결과를 $res_check2에 저장하고 그 결과가 존재한다면 조건문으로 넘어갑니다.

이 조건문의 끝이 window.history.back이므로 또 다시 조회수를 1 다운시켜주고

echo로 스크립트 태그를 출력합니다.

서버에서 php가 실행되고 그 다음 javascript인 script가 실행될 것이므로 이렇게 코딩해도 상관 없습니다.

script 태그 안쪽은 confirm으로 조건문을 나타내고 있는데 확인을 누르면 mysqli_multi_query로 두가지 쿼리문을 동시에 실행합니다.

$delete_like_query = "
UPDATE board SET thumbup= thumbup-1 WHERE number=$boardlike_id;
DELETE FROM like_manager WHERE like_board_id=$boardlike_id and username='$login_id';
";

board 테이블에서 좋아요 수 (thumbup)를 1 다운시키고 like_manager 테이블에서 WHERE 조건에 맞는 데이터를 삭제합니다. 취소를 누르면 비워둔 else로 빠지게 되므로 사용자는 좋아요 한 게시글이라는 것을 인지하고 되돌아갈 수 있습니다. 

 

본인의 게시글도 아니고 좋아요도 누르지 않은 게시글이라면 

세 번째 쿼리문이 실행됩니다.

$sql = "
UPDATE board SET thumbup =thumbup +1,hit=hit-1 WHERE number=$boardlike_id;
INSERT INTO like_manager(like_board_id, username) VALUES ($boardlike_id, '$login_id');
";
$res = mysqli_multi_query($con, $sql);

좋아요를 1 증가시키고 조회수는 되돌아갈 것이므로 1 다운시킵니다.

그리고 like_manager 테이블에 본인의 아이디와 게시글 넘버를 추가해줍니다.

쿼리문 실행은 두가지 이상이므로 mysqli_multi_query 함수로 실행합니다.

 

 

확실히 like_manager를 생성하여 관리하니 좀 더 익숙하게 그리고 쉽게 코딩한 것 같네요

 

한번 게시판에서 테스트해보겠습니다.

 

현재 bg5294 로 접속해있고 다른 글들은 전부 이 아이디가 작성한거라 좋아요를 누를 수 없으니

hacker라는 작성자가 게시한 글에 좋아요를 눌러봤습니다.

 

전부 1이라고 잘 뜨네요

DB에서도 잘 저장된 것을 볼 수 있습니다.

 

 

그리고 세션 아이디와 비교 검증되어 본인이 작성한 게시글에서는 좋아요 버튼이 보이지 않습니다.

 

좋아요 수가 얼마인지만 나타나지고 있네요

 

DB의 좋아요 데이터를 삭제하고 한번 다시 좋아요를 눌러보겠습니다.

 

좋아요를 눌렀다는 알림창이 뜨고,

조회수는 그대로 유지된채 좋아요 수가 1 증가하고 엄지 색깔이 바뀌었습니다.

이 상태에서 한 번 더 누르게 되면

이미 좋아요 한 게시글이라는 confirm 창이 뜹니다.

확인을 누르면,

 

엄지 색깔이 검정색으로 바뀌고 조회수는 그대로 유지된채 좋아요 수가 0으로 바뀐 것을 볼 수 있습니다.

 

 

 

생각보다 정말정말 오래걸린 좋아요 / 좋아요 취소 기능 구현입니다...

봐주셔서 감사합니다.


소스 참조

https://www.cho-log.io/53