반응형

요즘의 대부분의 커뮤니티들은 사용자가 작성한 글에 댓글이 달린 경우 이 사실을 작성자에게 알리는 기능이 있다. 내가 작성한 글에 댓글이 달리면 실시간으로 알려주는 기능을 만들어보자.

 

이런 기능은 페이지를 리로드 하거나 페이지를 리로드 하지 않도록 하기 위해 웹소켓을 이용하거나 SSE를 이용하거나 할 수 있는데 여기서는 SSE를 이용해서 처리해보도록 하겠다. 우선 index.php를 수정한다.

 

/index.php

<?php
include $_SERVER["DOCUMENT_ROOT"]."/inc/header.php";

$search_keyword = $_GET['search_keyword'];

if($search_keyword){
    $search_where = " and (subject like '%".$search_keyword."%' or content like '%".$search_keyword."%')";
}

$pageNumber  = $_GET['pageNumber']??1;//현재 페이지, 없으면 1
if($pageNumber < 1) $pageNumber = 1;
$pageCount  = $_GET['pageCount']??10;//페이지당 몇개씩 보여줄지, 없으면 10
$firstPageNumber  = $_GET['firstPageNumber'];
if($firstPageNumber < 1) $firstPageNumber = 1;

    $multi=$_GET['multi']??"free";//게시판 구분자
    $boards = new Boards($multi);//인스턴스를 생성한다
    $rsc=$boards->boardLists($multi, $search_keyword, $pageNumber, $pageCount);//클래스에 있는 함수를 이용해 게시물 리스트를 가져온다.
//    print_r($rsc);
?>
        <div><?php //클래스 적용부분
            $bi=$boards->boardInfo();
            echo $bi["boardName"]." ( 총 게시물수 : ".$bi["boardCount"]." / 마지막등록일 : ".$bi["boardLastDate"].")";
        ?></div>
        <?php
            if($_SESSION['UID']){//로그인 한 경우만 댓글 여부를 알려준다.
        ?>
            <div style="text-align:right;font-weight:600;">댓글 : <span id="memocnt">0<span></div>
        <?php }?>
        <!-- 더보기 버튼을 클릭하면 다음 페이지를 넘겨주기 위해 현재 페이지에 1을 더한 값을 준비한다. 더보기를 클릭할때마다 1씩 더해준다. -->
        <input type="hidden" name="nextPageNumber" id="nextPageNumber" value="<?php echo $pageNumber+1;?>">
        <table class="table">
        <thead>
            <tr>
            <th scope="col">번호</th>
            <th scope="col">글쓴이</th>
            <th scope="col">썸네일</th>
            <th scope="col">제목</th>
            <th scope="col">등록일</th>
            </tr>
        </thead>
        <tbody id="board_list">
            <?php
                $totalCount = Boards::totalCount($multi,$search_keyword);//전체 게시물 수를 가져오는 클래스
                $idNumber = $totalCount - ($pageNumber-1)*$pageCount;
                foreach($rsc as $r){
                    //검색어만 하이라이트 해준다.
                    $subject = str_replace($search_keyword,"<span style='color:red;'>".$search_keyword."</
                        span>",$r->subject);
                   
            ?>

                <tr>
                    <th scope="row"><?php echo $idNumber--;?></th>
                    <td><?php echo member_name($r->userid);?></td>
                    <td><?php
                        if(!empty($r->thumb)){
                            echo "<img src='/data/".$r->thumb."' width='50'>";
                        }else{
                            echo "null";
                        }
                    ?></td>
                    <td>
                        <?php
                            if($r->parent_id){
                                echo "&nbsp;&nbsp;<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"16\" height=\"16\" fill=\"currentColor\" class=\"bi bi-arrow-return-right\" viewBox=\"0 0 16 16\">
                                <path fill-rule=\"evenodd\" d=\"M1.5 1.5A.5.5 0 0 0 1 2v4.8a2.5 2.5 0 0 0 2.5 2.5h9.793l-3.347 3.346a.5.5 0 0 0 .708.708l4.2-4.2a.5.5 0 0 0 0-.708l-4-4a.5.5 0 0 0-.708.708L13.293 8.3H3.5A1.5 1.5 0 0 1 2 6.8V2a.5.5 0 0 0-.5-.5z\"/>
                              </svg>";
                            }
                        ?>  
                    <a href="/view.php?bid=<?php echo $r->bid;?>"><?php echo $subject?></a>
                    <?php if($r->filecnt){?>
                        <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-card-image" viewBox="0 0 16 16">
                        <path d="M6.002 5.5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z"/>
                        <path d="M1.5 2A1.5 1.5 0 0 0 0 3.5v9A1.5 1.5 0 0 0 1.5 14h13a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-13zm13 1a.5.5 0 0 1 .5.5v6l-3.775-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12v.54A.505.505 0 0 1 1 12.5v-9a.5.5 0 0 1 .5-.5h13z"/>
                        </svg>
                    <?php }?>
                    <?php if($r->memocnt){?>
                        <span <?php if((time()-strtotime($r->memodate))<=86400){ echo "style='color:red;'";}?>>
                            [<?php echo $r->memocnt;?>]
                        </span>
                    <?php }?>
                    <?php if($r->newid){?>
                        <span class="badge bg-danger">New</span>
                    <?php }?>
                </td>
                    <td><?php echo $r->regdate?></td>
                </tr>
            <?php }?>
        </tbody>
        </table>
        <!-- <div class="d-grid gap-2" style="margin:20px;">
            <button class="btn btn-secondary" type="button" id="more_button">더보기</button>
        </div> -->
        <form method="get" action="<?php echo $_SERVER["PHP_SELF"]?>">
        <div class="input-group mb-12" style="margin:auto;width:50%;">
                <input type="text" class="form-control" name="search_keyword" id="search_keyword" placeholder="제목과 내용에서 검색합니다." value="<?php echo $search_keyword;?>" aria-label="Recipient's username" aria-describedby="button-addon2">
                <button class="btn btn-outline-secondary" type="button" id="search">검색</button>
        </div>
        </form>
        <p>
            <?php echo Boards::paging($multi, $search_keyword, $pageNumber, $pageCount, $firstPageNumber);?>
        </p>

        <p style="text-align:right;">

            <?php
                if($_SESSION['UID']){
            ?>
                <a href="write.php?multi=<?php echo $multi;?>"><button type="button" class="btn btn-primary">등록</button><a>
                <a href="/member/logout.php"><button type="button" class="btn btn-primary">로그아웃</button><a>
            <?php
                }else{
            ?>
                <a href="/member/login.php"><button type="button" class="btn btn-primary">로그인</button><a>
                <a href="/member/signup.php"><button type="button" class="btn btn-primary">회원가입</button><a>
            <?php
                }
            ?>
        </p>

<script>
<?php
    if($_SESSION['UID']){//로그인 한 경우만 작동한다.
?>
    if(typeof(EventSource) !== "undefined") {//sse가 가능한지 확인
        var source = new EventSource("memocnt_ajax.php");//sse를 통해 파일을 읽어온다 get방식으로 변수를 넘길수도 있다.
        source.onmessage = function(event) {    
            $("#memocnt").text(event.data);//ajax파일에서 넘겨준 값을 표시한다.
        };
    }
<?php }?>
   
</script>

<?php
include $_SERVER["DOCUMENT_ROOT"]."/inc/footer.php";

댓글이란 것이니까 로그인한 회원인 경우에 한해서만 작동하면 된다. 이번엔 댓글이 몇개 달렸는지 여부를 확인할 수 있는 파일을 만들어보자.

 

/memocnt_ajax.php

<?php session_start();
include $_SERVER["DOCUMENT_ROOT"]."/inc/dbcon.php";
header('Content-Type: text/event-stream');//필수적으로 넣어준다.
header('Cache-Control: no-cache');//필수적으로 넣어준다.
$query="select count(*) from board b
        join memo m on b.bid=m.bid
        where b.userid='".$_SESSION['UID']."' and m.status=1 and m.isread=0";
$memo_cnt = $mysqli->query($query) or die("query error => ".$mysqli->error);
$rs = $memo_cnt->fetch_array();
$cnt = $rs[0];    

// retry와 data 뿌려줌
// retry뒤에 있는 숫자가 리로딩 되는 시간이다. 1000이면 1초다
echo "retry:1000\ndata:".$cnt."\n\n";

flush();

?>

간단하다. 이런식으로 작업하면 페이지를 리로드하지 않고 페이지에 머물러 있어도 디비의 변화값을 가져와서 뿌려줄 수 있다. 

 

그리고 isread란 값은 게시자가 쓴 글에 단 댓글을 읽지 않았단 뜻이므로 게시글의 댓글을 읽으면 이 상태값을 바꿔준다.

 

/view.php

<?php
include $_SERVER["DOCUMENT_ROOT"]."/inc/header.php";

$bid=$_GET["bid"];
//게시물의 정보를 가져온다.
$result = $mysqli->query("select * from board where bid=".$bid) or die("query error => ".$mysqli->error);
$rs = $result->fetch_object();

if($rs->userid==$_SESSION['UID']){//해당 게시물이 내가 쓴 게시물이면 댓글의 isread값을 모두 변경한다.
  $sql="update memo set isread=1 where isread=0 and bid=".$rs->bid;
  $result=$mysqli->query($sql) or die($mysqli->error);
}

//댓글 정보를 가져온다.
$query="select *, m.userid, m.regdate, m.memoid from memo m
left join file_table_memo f on m.memoid=f.memoid
where m.status=1 and m.bid=".$rs->bid." order by m.memoid asc";//메모에 꼭 첨부파일이 있는 것은 아니므로 left join을 사용한다.
$memo_result = $mysqli->query($query) or die("query error => ".$mysqli->error);
while($mrs = $memo_result->fetch_object()){
    $memoArray[]=$mrs;
}

//게시물의 첨부파일 정보를 가져온다.
$fquery="select * from file_table where status=1 and bid=".$rs->bid." and memoid is null order by fid asc";
$file_result = $mysqli->query($fquery) or die("query error => ".$mysqli->error);
while($frs = $file_result->fetch_object()){
    $fileArray[]=$frs;
}

//좋아요. 싫어요. 정보를 가져온다.
$query2="select type,count(*) as cnt from recommend r where bid=".$rs->bid." group by type";
$rec_result = $mysqli->query($query2) or die("query error => ".$mysqli->error);
while($recs = $rec_result->fetch_object()){
  $recommend[$recs->type] = $recs->cnt;
}

?>

<style>
  img {
    max-width:90%;
  }
</style>

      <h3 class="pb-4 mb-4 fst-italic border-bottom" style="text-align:center;">
        - 게시판 보기 -
      </h3>

      <article class="blog-post">
        <h2 class="blog-post-title"><?php echo $rs->subject;?></h2>
        <p class="blog-post-meta"><?php echo $rs->regdate;?> by <a href="#"><?php echo $rs->userid;?></a></p>

        <hr>
        <?php
          foreach($fileArray as $fa){
        ?>
          <p><img src="/data/<?php echo $fa->filename;?>"></p>
        <?php }?>
        <p>
          <?php echo $rs->content;?>
        </p>
       
        <div style="text-align:center;">
          <button type="button" class="btn btn-lg btn-primary" id="like_button">추천&nbsp;<span id="like"><?php echo number_format($recommend['like']);?></span></button>
          <button type="button" class="btn btn-lg btn-warning" id="hate_button">반대&nbsp;<span id="hate"><?php echo number_format($recommend['hate']);?></span></button>
        </div>
        <hr>
      </article>

     

      <nav class="blog-pagination" aria-label="Pagination">
        <a class="btn btn-outline-secondary" href="/index.php">목록</a>
        <a class="btn btn-outline-secondary" href="/write.php?parent_id=<?php echo $rs->bid;?>">답글</a>
        <a class="btn btn-outline-secondary" href="/write.php?bid=<?php echo $rs->bid;?>">수정</a>
        <a class="btn btn-outline-secondary" href="/delete.php?bid=<?php echo $rs->bid;?>" onclick="return confirm('삭제하시겠습니까?');">삭제</a>
      </nav>

      <div style="margin-top:20px;">
        <form class="row g-3">
          <input type="hidden" name="file_table_id" id="file_table_id" value="">
          <div class="col-md-8">
            <textarea class="form-control" placeholder="댓글을 입력해주세요." id="memo" style="height: 60px"></textarea>
          </div>
          <div class="col-md-2">
            <button type="button" class="btn btn-secondary" id="memo_button">댓글등록</button>
          </div>
          <div class="col-md-2" id="memo_image">
            <input type="file" name="upfile" id="upfile" />
          </div>
        </form>
      </div>
      <div id="memo_place">
      <?php
        foreach($memoArray as $ma){
      ?>
        <div class="card mb-4" id="memo_<?php echo $ma->memoid?>" style="max-width: 100%;margin-top:20px;">
          <div class="row g-0">
            <div class="col-md-12">
              <div class="card-body">
                <p class="card-text">
                <?php
                if($ma->filename){
                ?>
                <img src="/data/<?php echo $ma->filename;?>" style="max-width:90%;">
                <?php }?>
                <br>  
                <?php echo $ma->memo;?></p>
                <p class="card-text"><small class="text-muted"><?php echo $ma->userid;?> / <?php echo $ma->regdate;?></small></p>
                <p class="card-text" style="text-align:right"><a href="javascript:;" onclick="memo_modi(<?php echo $ma->memoid?>)">수정</a> / <a href="javascript:;" onclick="memo_del(<?php echo $ma->memoid?>)">삭제</a></p>
              </div>
            </div>
          </div>
        </div>
      <?php }?>
      </div>

<script>

  $("#upfile").change(function(){

    var files = $('#upfile').prop('files');
    for(var i=0; i < files.length; i++) {
        attachFile(files[i]);
    }

    $('#upfile').val('');

  });  

  function attachFile(file) {
    var formData = new FormData();
    formData.append("savefile", file);
    $.ajax({
        url: 'save_image_memo.php',
        data: formData,
        cache: false,
        contentType: false,
        processData: false,
        dataType : 'json' ,
        type: 'POST',
        success: function (return_data) {
            if(return_data.result=="member"){
                alert('로그인 하십시오.');
                return;
            }else if(return_data.result=="size"){
                alert('10메가 이하만 첨부할 수 있습니다.');
                return;
            }else if(return_data.result=="image"){
                alert('이미지 파일만 첨부할 수 있습니다.');
                return;
            }else if(return_data.result=="error"){
                alert('첨부하지 못했습니다. 관리자에게 문의하십시오.');
                return;
            }else{
                $("#file_table_id").val(return_data.fid);
                var html = "<div class='col' id='f_"+return_data.fid+"'><div class='card h-100'><img src='/data/"+return_data.savename+"' class='card-img-top'><div class='card-body'><button type='button' class='btn btn-warning' onclick='file_del("+return_data.fid+")'>삭제</button></div></div></div>";
                $("#upfile").hide();
                $("#memo_image").append(html);
            }
        }
    });

  }

  function file_del(fid){

    if(!confirm('삭제하시겠습니까?')){
    return false;
    }

    var data = {
        fid : fid
    };
        $.ajax({
            async : false ,
            type : 'post' ,
            url : 'file_delete.php' ,
            data  : data ,
            dataType : 'json' ,
            error : function() {} ,
            success : function(return_data) {
                if(return_data.result=="member"){
                    alert('로그인 하십시오.');
                    return;
                }else if(return_data.result=="my"){
                    alert('본인이 작성한 글만 삭제할 수 있습니다.');
                    return;
                }else if(return_data.result=="no"){
                    alert('삭제하지 못했습니다. 관리자에게 문의하십시오.');
                    return;
                }else{
                    $("#file_table_id").val('');
                    $("#f_"+fid).hide();
                    $("#upfile").show();
                }
            }
    });

    }

  $("#like_button").click(function () {

    if(!confirm('추천하시겠습니까?')){
      return false;
    }
     
      var data = {
          type : 'like' ,
          bid : <?php echo $bid;?>
      };
          $.ajax({
              async : false ,
              type : 'post' ,
              url : 'like_hate.php' ,
              data  : data ,
              dataType : 'json' ,
              error : function() {} ,
              success : function(return_data) {
                if(return_data.result=="member"){
                  alert('로그인 하십시오.');
                  return;
                }else if(return_data.result=="check"){
                  alert('이미 추천이나 반대를 하셨습니다.');
                  return;
                }else if(return_data.result=="no"){
                  alert('다시한번 시도해주십시오.');
                  return;
                }else{
                  $("#like").text(return_data.cnt);
                }
              }
          });
  });

  $("#hate_button").click(function () {

    if(!confirm('반대하시겠습니까?')){
      return false;
    }
     
      var data = {
          type : 'hate' ,
          bid : <?php echo $bid;?>
      };
          $.ajax({
              async : false ,
              type : 'post' ,
              url : 'like_hate.php' ,
              data  : data ,
              dataType : 'json' ,
              error : function() {} ,
              success : function(return_data) {
                if(return_data.result=="member"){
                  alert('로그인 하십시오.');
                  return;
                }else if(return_data.result=="check"){
                  alert('이미 추천이나 반대를 하셨습니다.');
                  return;
                }else if(return_data.result=="no"){
                  alert('다시한번 시도해주십시오.');
                  return;
                }else{
                  $("#hate").text(return_data.cnt);
                }
              }
          });
  });


  $("#memo_button").click(function () {
        var file_table_id = $("#file_table_id").val();
        var memo = $('#memo').val();
        if(!memo){
          alert('댓글을 입력하세요.');
          return false;
        }
        var data = {
            method : 'Memos::memoWrite',
            params: {memo: memo, bid: <?php echo $bid;?>, file_table_id: file_table_id}
        };
            $.ajax({
                async : false ,
                type : 'post' ,
                url : '/lib/routes.php' ,
                data  : data ,
                dataType : 'html' ,
                error : function() {} ,
                success : function(return_data) {
                  if(return_data=="member"){
                    alert('로그인 하십시오.');
                    return;
                  }else{
                    $('#memo').val('')
                    $("#file_table_id").val('');
                    $("#f_"+file_table_id).hide();
                    $("#upfile").show();
                    $("#memo_place").append(return_data);
                  }
                }
        });
    });

  function memo_del(memoid){

    if(!confirm('삭제하시겠습니까?')){
      return false;
    }
   
    var data = {
      method : 'Memos::memoDelete',
      params: {memoid: memoid}
    };
        $.ajax({
            async : false ,
            type : 'post' ,
            url : '/lib/routes.php' ,
            data  : data ,
            dataType : 'json' ,
            error : function() {} ,
            success : function(return_data) {
              if(return_data.result=="member"){
                alert('로그인 하십시오.');
                return;
              }else if(return_data.result=="my"){
                alert('본인이 작성한 글만 삭제할 수 있습니다.');
                return;
              }else if(return_data.result=="no"){
                alert('삭제하지 못했습니다. 관리자에게 문의하십시오.');
                return;
              }else{
                $("#memo_"+memoid).hide();
              }
            }
    });

  }

  function memo_modi(memoid){

    var data = {
        memoid : memoid
    };
   
    $.ajax({
          async : false ,
          type : 'post' ,
          url : 'memo_modify.php' ,
          data  : data ,
          dataType : 'html' ,
          error : function() {} ,
          success : function(return_data) {
            if(return_data=="member"){
              alert('로그인 하십시오.');
              return;
            }else if(return_data=="my"){
              alert('본인이 작성한 글만 수정할 수 있습니다.');
              return;
            }else if(return_data=="no"){
              alert('수정하지 못했습니다. 관리자에게 문의하십시오.');
              return;
            }else{
              $("#memo_"+memoid).html(return_data);
            }
          }
    });

  }

  function memo_modify(memoid){

    var data = {
        memoid : memoid,
        memo : $('#memo_text_'+memoid).val()
    };

    $.ajax({
          async : false ,
          type : 'post' ,
          url : 'memo_modify_update.php' ,
          data  : data ,
          dataType : 'html' ,
          error : function() {} ,
          success : function(return_data) {
            if(return_data=="member"){
              alert('로그인 하십시오.');
              return;
            }else if(return_data=="my"){
              alert('본인이 작성한 글만 수정할 수 있습니다.');
              return;
            }else if(return_data=="no"){
              alert('수정하지 못했습니다. 관리자에게 문의하십시오.');
              return;
            }else{
              $("#memo_"+memoid).html(return_data);
            }
          }
    });

    }

</script>

<?php
include $_SERVER["DOCUMENT_ROOT"]."/inc/footer.php";
?>

 

물론 이렇게해도 디비를 계속 적으로 읽어야 하므로 디비의 부하는 어쩔 수 없는 부분이다. 엘라스틱서치같은걸 이용해서 디비 부하를 줄일수도 있다. 또는 리로딩 시간을 조절해서 디비 부하를 최소화할 수도 있을것이다.

 

요즘은 프론트엔드 기술이 발전해서 앵귤러나 뷰같은 놈을 이용하는 경우가 많다. php의 이런 기능은 진짜 기본적으로 또는 아주 기초적으로 디비 부하를 주지 않는 선에서 사용해야 할 것이다. 

 

반응형

+ Recent posts