강원 by아포리아 postedApr 18, 2017

코어 수정 없이, 시조 댓글의 리스트만 출력하고, 자손 댓글의 리스트는 로드하지 않았다가 클릭 이벤트로 ajax 호출하기

Views 1892 Likes 0 Replies 0
Extra Form
입금자 성명 윤삼
필요장비 냉수, 에어컨
대관시간 1300
신청공간 세미나실
대관일 2011-10-10

개요

- 주지하시다시피 일반적으로 XE의 댓글 목록 출력 체계는 1차적 댓글1)과 그에 딸려 있는 자식 댓글 그리고 그 자식의 자식 댓글들로 이루어져 있습니다.

- 그런데 최근 포털사이트들(다음과 네이버)의 뉴스 페이지 등에서는 1차 댓글만 보여주고, 그에 대한 답글(즉, 자식 댓글)은 로드하지 않았다가 답글 링크를 클릭했을 때에야 답글 리스트를 볼 수 있습니다(페북도 이렇지 않던가요?).

- 이 글은 제목 그대로, 코어 수정을 따로 하지 않고, 포털사이트나 페북처럼 댓글단을 꾸미는 방법을 소개합니다.

- 주의 : 이 팁은 라이믹스 + 스케치북 스킨(모바일 뷰를 따로 사용하지 않음)에서 시연된 바 있습니다. 그 외의 환경에서는 적용되지 않을 수 있으므로 유의하시기 바랍니다.

 

00.png 01.png

 

 

 

쿼리 만들기: 시조 댓글의 카운트와 리스트

- 댓글단에서 (대댓글이 아닌) 최상위의 시조 댓글2)만 모아서 리스트로 만들기 위해선 몇 가지 준비물이 필요합니다.

 

- 아래처럼 쿼리를 짜고, 게시판 스킨에 queries라는 이름의 폴더를 만들어, 그 안에 넣습니다. 파일 이름은 getCommentCountByDepth0.xml으로 합니다.

<query id="getCommentCountByDepth0" action="select">
    <tables>
        <table name="comments_list" />
    </tables>
    <columns>
        <column name="count(*)" alias="count" />
    </columns>
    <conditions>
        <condition operation="equal" column="comments_list.document_srl" var="document_srl" filter="number" pipe="and" />
        <condition operation="equal" column="comments_list.depth" default="0" pipe="and" />
    </conditions>
</query>

- 참고로 이 파일은 특정 문서의 댓글 중에서 depth가 0인 시조 댓글의 개수를 구해주는 쿼리입니다.

 

- 이번에는 시조 댓글의 리스트를 불러오는 쿼리를 짜봅니다. 아래와 같이 하고, 파일 이름은 getCommentPageListByDepth0.xml으로 합니다.

<query id="getCommentPageListByDepth0" action="select">
    <tables>
        <table name="comments" alias="comments" />
        <table name="comments_list" alias="comments_list" />
    </tables>
    <columns>
        <column name="comments.*" />
        <column name="comments_list.depth" alias="depth" />
    </columns>
    <conditions>
        <condition operation="more" column="comments.status" var="status" pipe="and" />
        <condition operation="equal" column="comments_list.document_srl" var="document_srl" notnull="notnull" pipe="and" />
        <condition operation="equal" column="comments_list.comment_srl" var="comments.comment_srl" filter="number" pipe="and" />
        <condition operation="equal" column="comments_list.depth" default="0" pipe="and" />
    </conditions>
    <navigation>
        <index var="list_order" default="comments_list.comment_srl" order="desc" />
        <list_count var="list_count" default="list_count" />
        <page_count var="page_count" default="10" />
        <page var="page" default="1" />
    </navigation>
</query>

- 위에서 index 태그를 보면 댓글 목록의 정렬 순서가 desc로 되어 있는 걸 볼 수 있는데요, 이것은 댓글 목록을 최신순으로 보여준다는 걸 의미합니다(XE의 일반적인 목록 출력 방식은 오래된 순서로 보여주죠).

 

 

쿼리 만들기: 자손 댓글의 카운트와 리스트

- 이번에는 두 번째 준비물로 각각의 시조 댓글에 딸려 있는 자손 댓글3)을 불러올 쿼리를 짜보겠습니다.

 

- 아래와 같이 하고, 파일 이름은 getDescendantCommentCount.xml으로 합니다. 위에서와 같은 폴더에 저장하면 됩니다.

<query id="getDescendantCommentCount" action="select">
    <tables>
        <table name="comments_list" />
    </tables>
    <columns>
        <column name="count(*)" alias="count" />
    </columns>
    <conditions>
        <condition operation="equal" column="comments_list.head" var="comment_srl" filter="number" pipe="and" />
        <condition operation="more" column="comments_list.depth" default="1" pipe="and" />
    </conditions>
</query>

- 여기서 head값이란 바로 시조 댓글의 댓글번호를 말합니다. 따라서 이 xml은 차후에 입력할 댓글번호와 같은 head값의 댓글을 가져온다는 이야기입니다.

- 단, 시조 댓글 또한 자손 댓글과 동일한 head값을 가지므로, 이 쿼리에서는 depth가 1 이상인 댓글만 가져옵니다. 자연히 시조 댓글을 제외한 자손 댓글 전체의 개수가 카운트됩니다.

 

- 이번에는 자손 댓글의 리스트를 불러오는 쿼리를 짜보겠습니다. 파일 이름은 getDescendantCommentList.xml으로 합니다.

<query id="getDescendantCommentList" action="select">
    <tables>
        <table name="comments" alias="comments" />
        <table name="comments_list" alias="comments_list" />
    </tables>
    <columns>
        <column name="comments.*" />
        <column name="comments_list.depth" alias="depth" />
        <column name="comments_list.head" alias="head" />
    </columns>
    <conditions>
        <condition operation="more" column="comments.status" var="status" pipe="and" />
        <condition operation="equal" column="comments_list.comment_srl" var="comments.comment_srl" filter="number" pipe="and" />
        <condition operation="equal" column="comments_list.head" var="comment_srl" filter="number" pipe="and" />
        <condition operation="more" column="comments_list.depth" default="1" pipe="and" />
    </conditions>
    <navigation>
        <index var="list_order" default="comments_list.comment_srl" order="asc" />
        <list_count var="list_count" default="list_count" />
        <page_count var="page_count" default="10" />
        <page var="page" default="1" />
    </navigation>
</query>

- 우선 이 리스트는 댓글 번호를 오름차순, 즉 오래된 순서로 정렬합니다. 최신순으로 바꾸고 싶으면 index 태그에서 asc 대신 desc를 넣으면 됩니다.

- 이후에 살펴보겠지만 이 글의 팁은 일단 대댓글에서는 페이지네이션을 따로 출력하지 않을 생각입니다(이 부분은 아직 개발 중이거든요). 따라서 리스트의 첫 페이지는 1페이지로 고정했습니다. 혹시 이 부분을 활용하고자 한다면 page 태그를 지우시는 것도 방법입니다.

- 불러올 칼럼에 head값이 포함된 걸 볼 수 있습니다. 이건 후에 스킨을 수정하거나 하실 때 도움을 드릴 수 있을까 해서 포함해봤습니다. 필요가 없으시다면 <column name="comments_list.head" alias="head" />를 지우시면 됩니다.

 

 

스킨 수정: 시조 댓글을 위한 '변수 생성 및 조작'

- 이번에는 스킨 폴더에서 _comment.html 파일을 엽니다. 쿼리를 스킨으로 실행해야 하니까요.

 

- 아래의 구문을 파일 맨 앞에 넣어줍니다.

{@
    $oModuleModel = getModel('module');
    $comment_config = $oModuleModel->getModulePartConfig('comment',$module_info->module_srl);
    $query_path = $module_info->module.'/skins/'.$module_info->skin;

    $args = new stdClass();
    $args->document_srl = $oDocument->document_srl;
    $ccbd = executeQuery($query_path.'.getCommentCountByDepth0', $args);
    if(Context::get('cpage')):
        $cpage = Context::get('cpage');
    else:
        $cpage = 1;
    endif;
    $args->list_count = $comment_config->comment_count;
    $args->page = $cpage;

    $output = executeQuery($query_path.'.getCommentPageListByDepth0', $args);
    foreach($output->data as $key => $val):
        $oCommentItem = new commentItem();
        $oCommentItem->setAttribute($val);
        $comment_list[$val->comment_srl] = $oCommentItem;
    endforeach;

    if($output->total_page>1) $oDocument->comment_page_navigation = $output->page_navigation;
}

- 여기서 $comment_config는 게시판 관리 페이지에서 댓글 설정 부분에 해당합니다. $comment_config->comment_count는 한 페이지에 출력되는 댓글의 개수를 의미합니다.

- $query_path는 좀 전에 만들어 저장했던 xml 파일들이 저장된 경로를 의미합니다.

- $ccbd는 getCommentCountByDepth0 쿼리를 실행시켜 만든 변수로서 시조 댓글의 개수를 뜻합니다.

- $output이 바로 시조 댓글의 리스트에 해당하는 변수죠. 일반적으로는 $output->data를 반복문으로 돌려서 필요한 변수를 출력하곤 합니다. 그러나 이 변수로는 댓글 작성자의 프로필 이미지라든가, comment 모듈에서 활용되는 몇몇 중요한 아이템을 불러올 수는 없습니다. 그래서 foreach에서 보듯, $comment_list라는 새로운 배열 변수를 만들어 그 안에 comment 모듈에서 활용하는 아이템 함수들을 연결시켜줍니다.

 

- 참고 : 물론, <li loop="$oDocument->getComments()=>$key,$comment" cond="$comment->get('depth')==0">처럼 일반적인 댓글 목록에서도 시조 댓글만 출력을 해줄 수는 있습니다. 그러나 이렇게 하면 댓글 목록에서 자식 댓글이 빠진 채로 리스트 구성이 돼서 전체적인 모양새가 어그러지게 됩니다. 예컨대, 게시판 관리 댓글 설정에서 한 페이지에서 볼 수 있는 댓글 목록 수를 20으로 해놨을 때, depth가 1 이상인 자식 댓글이 17개라면 댓글 페이지에 로드된 댓글의 개수는 3개가 되는 거죠. 그러나 위의 쿼리로 템플릿 문법을 쓰면 시조 댓글을 원하는 개수만큼 출력하는 게 가능해집니다.

 

 

스킨 수정: 시조 댓글을 위한 '소스 수정'

- 이렇게 몇 가지 중요한 변수들을 생성했으니, 그에 맞게 출력 부분을 수정해주면 되겠습니다.

 

- 아래 그림처럼,리스트의 댓글 개수를 전체 댓글 개수가 아니라 시조 댓글 개수로 표현해주려면,

  1.png <b>'{$oDocument->getCommentcount()}'</b> 대신에 <b>'{$ccbd->data->count}'</b> 라고 해주면 됩니다.

 

- 그리고 소스 중에 아래와 같은 부분이 있을 건데요. (이게 정확하게 맞는지는 모르겠네요. 제가 지금 스케치북 순정 파일을 보면서 비교하고 있는 게 아니라서요. 그래도 대략 ul 태그와 li 태그 여는 부분이라 여기시면 되겠습니다)

<ul class="fdb_lst_ul {$mi->fdb_hide}">
    <block loop="$oDocument->getComments()=>$key,$comment">
    <!--@if($comment->get('depth'))-->
    <li id="comment_{$comment->comment_srl}" class="fdb_itm clear re bg{($comment->get('depth'))%2}" style="margin-left:{(($comment->get('depth')-1)%10+1)*2}%">
        <i class="fa fa-share fa-flip-vertical re"></i><i cond="$comment->get('depth')>10" class="fa fa-share fa-flip-vertical re rere"></i>
    <!--@else-->
    <li id="comment_{$comment->comment_srl}" class="fdb_itm clear">
    <!--@end-->

- 이 부분을 아래와 같이 바꿔줍니다.

<ul class="fdb_lst_ul {$mi->fdb_hide}">
    <block loop="$comment_list=>$key,$comment">
    <li id="comment_{$comment->comment_srl}" class="fdb_itm clear">

- block 태그에서 반복문의 변수가 $comment_list로 바뀐 것을 보실 수 있습니다.

- 또한, 이 소스는 어차피 시조 댓글(depth==0)만 모아놓은 것이기 때문에, depth 조건에 따라 li를 다르게 출력하는 부분은 없앴습니다.

 

 

스킨 수정: '자손 댓글'을 위한 소스 수정

- 이제 슬슬 시조 댓글에 딸린 자손 댓글들을 불러와야겠습니다. 이를 위해 몇 가지 _comment.html 파일을 조금 더 수정해보겠습니다.

 

- 먼저, 위의 li 태그 바로 아랫줄에 다음 구문을 삽입해줍니다.

{@
    $args = new stdClass();
    $args->comment_srl = $comment->comment_srl;
    $dcc = executeQuery($query_path.'.getDescendantCommentCount', $args);
}

- 이렇게 하면 시조 댓글에 딸린 자손 댓글의 개수를 구할 수 있겠죠?ㅎㅎ

 

- 소스를 잘 보시면 <div class="fdb_nav img_tx">라는 태그를 보실 수 있을 겁니다. 이 태그가 끝나는 부분(</div>)을 찾으시고 그 아래에 다음과 같은 코드를 삽입합니다.

    <!--// 답글 정보 -->
    <block cond="$dcc->data->count">
        <a class="meta_re_tg re_comment" href="#" style="text-decoration: none; background-color: #735763; color: white; letter-spacing: -1px; padding: 6px 7px; border-radius: 3px; opacity: 0.6;"><!--@if($lang_type=='ko')-->답글<!--@else-->Replies<!--@end--> ({$dcc->data->count})</a>
    </block>

- 만약 시조 댓글에 대한 자손 댓글이 5개가 존재한다면, 자주 빛깔의 살짝 둥그스룸한 박스가 생길 것이고, 그 안에는 흰 글씨로 답글 (5)라고 나타나게 될 겁니다.

- 또한, 이 소스는 시조 댓글의 최하단에서 자손 댓글을 불러오는 링크 역할을 합니다. 이 링크를 클릭하면 나중에 붙일 스크립트를 통해 자손 댓글의 리스트를 불러올 이벤트가 실행될 겁니다.

 

- 그리고 마지막으로 li 태그가 끝나는 부분(</li>) 바로 다음 줄에 다음과 같은 div 태그를 하나 넣어줍니다.

    <div class="comment_re_container" cond="$dcc->data->count" style="display:none"></div>

- 클래스명과 스타일을 보면 바로 아시겠죠? 그렇습니다. 위의 링크 버튼을 클릭하면 자손 댓글의 리스트가 이 div 태그 안으로 나오게 됩니다.

 

 

Ajax 호출: 자손 댓글 리스트 부르기

- 위에서 짠 getDescendantCommentList 쿼리를 이용한다면 자손 댓글 리스트 자체를 출력해주는 건 어렵지 않습니다. 그러나 이렇게 되면 자손 댓글의 양이 많을 경우 서버 부하도 염려되고 페이지 로딩 속도에도 영향을 줄 수 있습니다.

- 우리의 목적은 자손 댓글은 로드하지 않았다가, 위의 '답글' 링크를 누르면 그때 자손 댓글의 리스트를 로드하는 거였잖아요? 사실, 이 문제가 이 팁을 개발할 때 가장 까다로운 부분이었습니다. 몇 번을 헤매다가 라자루스님의 힌트로 XE코어에 getBoardCommentPage라는 액션이 있다는 걸 알게 됐죠. 즉, ajax로 이 액션을 실행시켜주면 댓글 리스트를 불러올 수 있다는 겁니다.

 

- 그럼, 일단 여기서는 ajax 호출 부분을 알려드리겠습니다. 아래의 코드를 _comment.html 파일의 맨 아래에 넣어주시면 되겠습니다.

<script>
(function($) {
    $(document).on('click', '.meta_re_tg', function(){
        var target = $(this).parent('li').next('.comment_re_container'),
            params = {
                document_srl : '{$oDocument->document_srl}',
                head : $(this).parent('li').attr('id').replace(/[^0-9]/g,''),
                descendant_count : $(this).text().replace(/[^0-9]/g,'')
            };
        $.ajax({
            type: 'POST',
            dataType: 'json',
            url: current_url,
            data: params,
            success: function() {
                exec_json('board.getBoardCommentPage', params, function(data) {
                    target.html(data.html);
                });
            }
        });
        target.fadeToggle('slow');
        return false;
    });
})(jQuery);
</script>

- getBoardCommentPage에 params 값을 넘겨주는 게 보이실 겁니다. 일단 document_srl은 해당 액션(getBoardCommentPage)에 적용되고, head와 descendant_count 값은 이 액션이 불러오는 페이지로 넘겨질 예정입니다.

- 자, 이제 어떻게 되느냐...?

 

 

더미 스킨 만들기: 자손 댓글을 위한 모바일 스킨 지정

- getBoardCommentPage 액션은 모바일 용으로 만들어진 것으로서, 게시판의 모바일 스킨으로 지정된 스킨에 있는 comment.html 파일을 로드해줍니다. 조금 복잡하죠?

- 그런데 말입니다. PC용 스킨에서 모바일 스킨의 파일을 불러온다...? 이건 어딘가 이상한 상황 아닙니까?4)

- 그래서!! 꼼수를 생각해봤습니다. 모바일 스킨 폴더를 가라로 하나 만들어놓고, 그 스킨을 모바일 스킨으로 지정하는 겁니다. 그리고 거기에 자손 댓글 리스트를 출력할 comment.html 파일만 만들어놓는 겁니다. 그러면...? 코어 수정을 하지 않은 채로 자손 댓글을 ajax 호출하는 게 가능해집니다!

 

- 모바일 스킨 폴더를 하나 만들고(/modules/board/m.skins/새폴더이름), 그 안에 다음과 같은 파일을 저장해둡니다.

  2.png 

- 여기서 comment.html 파일은 PC용 스케치북 스킨의 _comment.html  '원본' 파일을 모바일 더미 스킨의 comment.html로 저장해두는 것으로부터 시작하겠습니다.

- 어차피 스케치북의 css가 적용되니 다른 스킨의 comment.html로는 전혀 어울리지 않을 거예요. ^^

- 참고로, 제 경우에 skin.xml은 이런 코드로 되어 있습니다.

<?xml version="1.0" encoding="UTF-8"?>
<skin version="0.2">
    <title xml:lang="ko">SKETCHBOOK5-BLOG(Dummy for mobile)</title>
    <description xml:lang="ko">SKETCHBOOK5 - Blog Skin. 스케치북 스킨을 개조한 블로그형 스킨, 사용자 편의를 고려한 다양한 기능, 모바일까지 대응.</description>
    <version>1.7.2</version>
    <date>2017-04-16</date>
    <author email_address="contact@sketchbooks.co.kr" link="http://sketchbooks.co.kr/">
        <name xml:lang="ko">SKETCHBOOK</name>
    </author>
    <colorset>
        <color name="white">
            <title xml:lang="ko">하얀색</title>
        </color>
        <color name="black">
            <title xml:lang="ko">검은색</title>
        </color>
    </colorset>
</skin>

 

- 그리고 게시판 관리 페이지의 게시판 정보 탭에서 아래와 같이 해당 폴더를 선택해둡니다.

  3.png

- 스케치북 스킨은 반응형이니 따로 모바일 뷰를 사용하지 않곤 하죠? 이 팁에서도 '모바일 뷰 사용'은 체크가 해제되어 있어야 합니다. 그리고 모바일 스킨은 좀 전에 가라로 만들어놨던 스킨으로 선택되어 있어야 합니다. 그래야 그리로 연결이 되니까요.

 

 

더미 스킨 만들기: 자손 댓글 리스트를 위한 '변수 생성 및 조작'

- 그러면 이번에는 모바일 스킨 폴더의 comment.html 파일을 수정하겠습니다.

 

- comment.html 맨 위에 아래와 같은 코드를 넣습니다.

{@
    $mi = $module_info;
 

    $args = new stdClass();
    $args->document_srl = Context::get('document_srl');
    $args->comment_srl = Context::get('head');
    $args->list_count = Context::get('descendant_count');
    $args->page = 1;

    $query_path = $module_info->module.'/skins/'.$module_info->skin;

    $o_output = executeQuery($query_path.'.getDescendantCommentList', $args);
    $o_comment_list = [];
    foreach($o_output->data as $key => $val):
        $oCommentItem = new commentItem();
        $oCommentItem->setAttribute($val);
        $o_comment_list[$val->comment_srl] = $oCommentItem;
    endforeach;
}

- $mi는 스케치북 스킨에서 설정한 각종 변수들을 그대로 활용할 목적으로 선언해둔 겁니다.

- Context::get~~로 시작하는 변수들이 보이죠? 이게 바로 '6절. Ajax 호출' 부분에서 넘겨온 변수에 해당합니다. 얘네들을 가지고 자손 댓글들의 리스트를 불러들일 수 있습니다.

- 자손 댓글은 별도의 페이지네이션 없이 로드하도록 하겠습니다. 그러니 page는 그냥 '1'로 했습니다. page_count는 '5'로 되어 있는데, 여기 팁에서는 별 상관이 없긴 하지만 일단 언제든 재활용할 수 있으므로 그냥 남겨놨습니다.

- 그 외에는 앞에서 시조 댓글 변수를 조작하던 것과 크게 다르지 않습니다. $comment_list 대신 $o_comment_list라고 이름을 달리 했고, 하나의 자손 댓글 목록이 다른 자손 댓글 목록과 구별될 수 있도록 시조 댓글이 달라질 때마다 $o_comment_list를 초기화하는 점($o_comment_list = [];)이 조금 다르다면 다르다고 할 수 있습니다.

 

 

더미 스킨 만들기: 자손 댓글 리스트를 위한 '소스 수정'

- 자, 이제 마지막입니다. 이제 comment.html의 출력부를 수정해주면 되겠습니다.

 

- ul ~ li 태그가 열리는 부분이 있을 겁니다(앞의 4절의 원본 소스 부분 참조).

<ul class="fdb_lst_ul {$mi->fdb_hide}">
    <block loop="array_values($o_comment_list)=>$k,$cmt">
    <li id="comment_{$cmt->comment_srl}" class="fdb_itm clear re bg{($k+1)%2}" style="margin-left:30px">
        <i class="fa fa-share fa-flip-vertical re"></i>

- 이번에는 이렇게 바꿉니다. block 태그의 반복문 조건이 바뀐 걸 볼 수 있죠?

- 그리고 키와 변수도 $k와 $cmt로 축약된 걸 볼 수 있습니다. 이건 시조 댓글의 키와 변수($key, $comment)와 중복되지 않기 위함입니다.

- 스케치북 원본에서는 댓글의 depth에 따라 클래스명이 달라지고(bg0과 bg1) 그에 따라 댓글의 배경색이 달라집니다. 또 좌측 여백의 크기도 달라지죠.

- 그러나 여기 팁에서는 자손 댓글의 depth와 상관 없이 (1) 등록 순서에 따라 오름차순으로 정렬되고, (2) 좌측 여백은 30px로 고정되며, (3) 배경색은 홀짝 순번에 따라 달라집니다.

 

- 그리고 소스 파일에서 '$comment->'를 찾아서 모두 '$cmt->'로 바꿔주세요.

 

- 중간에 보면 댓글의 첨부파일을 불러오는 부분이 있을 겁니다. 'hasUploadedFiles()'라는 글자가 있는 부분이죠. 여기에 li 태그의 loop 속성을 보면 getUploadedFiles()=>$key,$file이라고 되어 있을 텐데요. 이 부분도 시조 댓글과의 구분을 위해 $k,$f 등으로 바꿔줍니다. 마찬가지로 '$file->'을 찾아서 모두 '$f->' 등으로 바꿔줘야 하겠습니다.

 

- 마지막으로, 하단의 '페이지네이션' 부분이 있을 텐데요. 이 부분은 사용하지 않는 부분이니 삭제하셔도 됩니다. 물론 훗날을 도모하기 위해 일단 남겨두셔도 좋구요.

 

 

... 이상입니다. 설명이 충분했는지 모르겠습니다. 팁을 정리하는 과정에서 혹시라도 누락된 부분이 있을지 모릅니다. 잘 안 되는 부분이 있다면 같이 머리를 맞대고 고민해보도록 하겠습니다.

 

각주

  1. 부모 댓글이 따로 없는, 즉 depth값이 0인 댓글을 이름

  2. 시조 댓글이란, 각주 1과 같은 것으로서 최상위 댓글, 1차 댓글, 원댓글 등으로 바꿔 부를 수 있다. 여기서는 그 하위의 자손 댓글들과의 언어적 구분을 위해 시조 댓글이라 하겠다.

  3. 자손 댓글이란, 시조 댓글이 아닌 것으로서 특정한 부모에 속해 있는 모든 하위의 댓글을 이른다.

  4. 원래는 그냥 PC용 스킨에서 별도의 파일을 만들어두고 이 파일을 불러올 방법을 모색했지만, 파일 퍼미션 문제 때문인지 잘 되지 않았다;;

 

?