코드이그나이터4 마크다운 블로그 MVP 만들기 - 10 - 글 기능에 회원 기능 넣기

고은연·2021년 10월 28일
0

글 기능 수정하기

이번 챕터의 코드는 https://github.com/koeunyeon/ci4/commits/blog-connect-post-member 에 있습니다.

기능이 거의 다 만들어졌습니다. 이제 글 기능에 회원 기능을 녹여넣으면 MVP는 완성입니다.

수정 목록

수정해야 할 목록을 리스트업 해 보겠습니다.

  • 생성 페이지에서 로그인 여부 검사
  • 생성 페이지에서 글쓴이 정보 넣기
  • 수정 페이지에서 로그인 여부 검사
  • 수정 페이지에서 글쓴이인지 검사
  • 삭제 페이지에서 로그인 여부 / 글쓴이인지 검사
  • 조회 페이지에서 글쓴이라면 수정 / 삭제 버튼 보이기
  • 생성 / 수정 페이지에서 취소하기 버튼 추가하기
  • 목록 페이지에서 로그인 되어 있다면 글쓰기 버튼 보이기
  • 레이아웃 페이지에 로그인 / 로그아웃 버튼 추가

엄청 많아서 귀찮을 것 같지만 대부분 반복되는 코드이기 때문에 사실 간단하게 복사 붙여넣기로 처리 가능합니다.

세션 헬퍼 만들기

코드이그나이터 4.0.4 버전에서는 파일 세션 관련 버그가 있는 것 같습니다. 만약 글로벌 세션 $_SESSION에 값이 있을 때 session() 헬퍼 함수로 세션을 시작하면, $_SESSION 안의 값들 중 키가 ci_로 시작하는 것들을 제외하고는 모두 없에버리더군요.
아마 내부적으로 코드이그나이터 세션 키는 ci_session을 이용하고 PHP는 PHPSESSID를 사용하므로 덮어쓰는게 아닐 까 생각합니다만, 우리가 원하는 동작은 이게 아니죠.
우리는 소셜 로그인을 사용할 때 부득이하게 글로벌 세션을 사용했으므로 글로벌 세션에 접근할 방법이 필요합니다. 따라서 글로벌 세션에 접근하는 헬퍼 클래스를 만들겠습니다.
app/Helpers/SessionGlobalHelper.php

<?php
namespace App\helpers;


class SessionGlobalHelper
{
    public static function start() // (1)
    {
        if (isset($_SESSION) == false) {  // (2)
            session_start();
        }
    }

    public static function set($key, $val) // (3)
    {
        self::start();
        $_SESSION[$key] = $val;
    }

    public static function get($key, $default = null)  // (4)
    {
        self::start();
        if (isset($_SESSION[$key])) {
            return $_SESSION[$key];
        }
        return $default;
    }

    public static function remove($key) // (5)
    {
        self::start();
        if (isset($_SESSION[$key])) {
            unset($_SESSION[$key]);
        }
    }
}

위 코드는 POPL - https://github.com/poplcode/popl 프로젝트의 세션 라이브러리를 가져와서 클래스 형식으로 수정한 것입니다.

꼭 코드이그나이터4의 문제가 아니라 어떤 언어의 어떤 프레임워크를 쓰더라도 이런 라이브러리 충돌 문제는 생깁니다. 프레임워크는 80%의 문제를 쉽게 해결할 수 있게 해 주지만, 나머지 20%의 프레임워크 문제는 80%의 문제를 쉽게 해결해서 아낀 시간만큼을 허비하게 하죠.
이런 20%의 문제를 해결하기 위해 특정 프레임워크를 쓰기 위해서는 그 프레임워크의 전문가가 필요해집니다.

(1) 세션을 시작합니다.
PHP는 여러군데에서 require되는 특성상 어느 파일에서 세션을 시작할 지 모릅니다. 코드이그나이터4 안에서야 전체 요청 흐름을 제어할 수 있겠지만 외부 라이브러리를 쓰는 순간 정상 작동하지 않을 가능성이 큽니다.
PHP에는 슈퍼 글로벌이라고 불리는 어떤 영역에도 속하지 않고 어디서든 접근할 수 있는 변수들이 존재하기 때문에, 슈퍼 글로벌 변수들에 대해서는 늘 검사가 필요합니다.

static이 메소드에 사용되면 정적 메소드라는 뜻입니다. 정적 메소드는 인스턴스의 메소드가 아니라 클래스의 메소드여서 클래스의 인스턴스를 생성하지 않고 곧바로 쓸 수 있습니다. 개인적으로는 멤버 변수가 없는(즉, 멤버 메소드만 있는) 인스턴스들는 모두 같은 상태를 가지므로 굳이 인스턴스화 할 필요가 없다고 생각해서 static method로 선언했습니다.

(2) 세션이 시작되었는지 검사하는 가장 간단한 방법은 $_SESSION값이 있는지 검사하는 겁니다. 세션이 시작하면 슈퍼 글로벌 변수 $_SESSION이 생기죠.

(3) 세션에 값을 넣습니다.

(4) 세션에서 값을 가져옵니다. 값이 없을 경우 $default의 값을 가져옵니다. $default의 기본값은 null이기 때문에, 값이 없고 두번째 파라미터가 없다면 null을 리턴합니다.

(5) 세션에서 값을 삭제합니다.

로그인 헬퍼 만들기

세션 헬퍼를 이용해 로그인 유무를 확인할 수 있는 헬퍼 함수를 만듭니다. 로그인 여부는 전역적으로 사용되기 때문에 간단한 함수를 만들어 두면 사용하기 편리합니다.

app/Helpers/LoginHelper.php

<?php
namespace App\helpers;

class LoginHelper
{
    public const MEMBER_ID = "member_id"; // (1)
    
    public static function isLogin() // (2)
    {
        return SessionGlobalHelper::get(self::MEMBER_ID) !== null; //(3)
    }

    public static function memberId(){ // (4)
        return SessionGlobalHelper::get(self::MEMBER_ID);
    }
}

(1) 로그인 여부를 확인하는 세션의 키가 member_id라는 것을 상수로 선언했습니다. 이렇게 선언해 두면 나중에 사용자를 식별하는 키가 변경되어야 할 때 한군데만 고치면 됩니다.

(2) 로그인되어 있는지 확인하는 정적 메소드입니다.

(3) 만약 글로벌 세션에 member_id가 있다면 로그인 된 것으로 판단합니다.

(4) 회원 정보를 가져옵니다. 세션에 있는 값을 바로 빼내도 될 텐데 굳이 메소드를 분리하는 이유는 이 편이 훨씬 코드를 읽기 쉽기 때문입니다. 코드를 사용하는 입장에서는 굳이 사용자를 식별하는 세션 키가 member_id라는 것을 알 필요가 없어지므로 한 층 추상화가 이루어집니다. 이정도 추상화는 구현하는 시간에 그닥 차이가 없으므로 일정에 영향을 주지 않는 수준으로만 추상화합시다.

생성 페이지에서 로그인 여부 검사

생성 페이지는 로그인한 사람만 접근 가능하게 수정하겠습니다. Post 컨트롤러의 create() 메소드를 수정합니다.
app/Controllers/Post.php

public function create()
{
    if (LoginHelper::isLogin() === false) { // (1)
        return $this->response->redirect("/post");
    }
    
    if ($this->request->getMethod() === "get") {
        return view("/post/create");
    }

    ... (생략)...

}

(1) 메소드가 시작할 때 로그인 여부를 검사하고 로그인이 되어 있지 않으면 글 목록 페이지로 이동합니다. 이 기능은 GET, POST 둘 다 적용됩니다.
조건식을 보면 ! LoginHelper::isLogin() 형태가 아니라 LoginHelper::isLogin() === false 형태로 되어 있는 것을 볼 수 있는데요. 저는 개인적으로 코드를 읽다가 ! 표시를 놓치고 코드를 잘못 해석한 일이 많아서 === false 형식으로 사용하는 것을 좋아합니다. 이 부분은 기호에 따라 다르니 편하실 대로 쓰시면 됩니다.


만약 if (LoginHelper::isLogin() 코드 중 LoginHelper아래에 빨간 지렁이줄이 간다면 LoginHelper에 커서를 두고 Ctrl + Space를 눌러서 자동으로 use문을 만들어내세요. 보통 PHPStorm은 자동으로 호출되는 코드를 넣어주지만, 어떤 오류로 인해 자동으로 use문이 채워지지 않는다면 클래스 선언 위에 아래의 선언문을 넣어주시면 됩니다.

use App\helpers\LoginHelper;

우리가 지금 테스트하는 브라우저는 이미 "소셜 로그인된 상태"일 겁니다. 따라서 로그인이 안되어 있을 때의 테스트가 어려울 거에요. 그래서 우리는 브라우저의 시크릿 모드를 이용하겠습니다.
크롬, 엣지에서는 ctrl + shift + n 키를, 파이어폭스에서는 ctrl + shift + p 키를 누릅니다.

시크릿 모드로 띄운 창에서 글 생성 페이지 - localhost:8080/post/create를 접속해 봅시다. 글 목록 페이지로 튕겨나옵니다.

생성 페이지에서 글쓴이 정보 넣기

글을 데이터베이스에 생성하는 기존 코드는 아래와 같았습니다.
app/Controllers/Post.php

$data = $this->add_input_markdown();
$post_id = $model->insert($data);

변경할 코드는 아래와 같습니다. 입력값을 그대로 넣는 것 외에 a.) 마크다운으로 변환한 html_contentadd_input_markdown으로 설정하고, b.) 세션에서 가져온 회원값 또한 데이터베이스에 넣게 됩니다.

$data = $this->add_input_markdown(); // (1)
$data['author'] = LoginHelper::memberId(); // (2)
$post_id = $model->insert($insert_data); // (3)

(1) 마크다운으로 변환된 글을 가져오는 기능은 동일합니다.
(2) 작가의 정보를 배열에 담습니다.
(3) 데이터를 저장합니다.


만들었으면 확인해 봐야 합니다. 당연히 될 것이라고 생각하는 믿음이 버그를 만듭니다.
http://localhost:8080/post/create

확인도 해 봐야죠.

수정 페이지에서 로그인 여부 검사

수정 페이지의 로그인 검사는 생성 페이지의 로그인 검사와 완전히 동일하기 때문에 복사 붙여넣기 하겠습니다.
app/Controllers/Post.php

public function edit($post_id)
{
    if (LoginHelper::isLogin() === false) { // (1)
        return $this->response->redirect("/post");
    }

수정 페이지에서 글쓴이인지 검사

수정 페이지는 글을 쓴 사람만 수정할 수 있기 때문에, 자기가 쓴 글인지도 검사해야 합니다.
Post.php 파일의 edit 메소드에 아래의 코드를 추가합니다.
app/Controllers/Post.php

$post = $model->find($post_id);
if (!$post) {
    return $this->response->redirect("/post");
}

// 추가된 코드
if ($post['author'] !== LoginHelper::memberId()){ // (1)
    return $this->response->redirect("/post");
}
// 추가된 코드 끝

if ($this->request->getMethod() === "get") {

(1) 글쓴이($post['author'])가 로그인한 사용자와 다르다면 글 목록으로 리다이렉트합니다. 이미 로그인 여부와 글이 있는지 여부는 확인했기 때문에 null 검사를 하지 않고 로직을 검증할 수 있습니다.


처음에 샘플로 넣은 글(1번 글)은 아직 저자 정보를 작성하지 않았기 때문에, author 컬럼이 비어있습니다. 이 점을 이용해서, 정말 자기가 쓴 글일 때만 수정 페이지를 접속할 수 있는지 확인해 봅시다.

글 수정 페이지 - localhost:8080/post/edit/1에 접속해 보면, 자동으로 글 목록 페이지 - http://localhost:8080/post로 튕겨나갑니다.

삭제 페이지에서 로그인 여부 / 글쓴이인지 검사

삭제 페이지도 수정 페이지와 동일하게 수정하겠습니다. 먼저 로그인 여부를 확인합니다.
app/Controllers/Post.php

public function delete()
{
    // 추가된 부분 (1) 시작
    if (LoginHelper::isLogin() === false) { // (1)
        return $this->response->redirect("/post");
    }
    // 추가된 부분 (1) 끝

글쓴이인지 확인합니다.

if (!$post) {
    return $this->response->redirect("/post");
}

// 추가된 코드 (2) 시작
if ($post['author'] !== LoginHelper::memberId()){
    return $this->response->redirect("/post");
}
// 추가된 코드 끝

조회 페이지에서 글쓴이라면 수정 / 삭제 버튼 보이기

컨트롤러에서 뷰에 데이터를 전달할 때 글쓴이인지 정보를 함께 전달해 보겠습니다. 기존의 show 메소드는 아래와 같은 값을 리턴했습니다.
app/Controllers/Post.php

return view('/post/show',[
    'post' => $post
]);

글쓴이 정보도 함께 뷰로 전달하도록 수정합시다.

$isAuthor = LoginHelper::isLogin() && $post['author'] == LoginHelper::memberId(); // (1)
return view('/post/show',[
    'post' => $post,
    'isAuthor' => $isAuthor
]);

(1) 조회 페이지는 로그인 여부를 사전에 확인하지 않기 때문에 로그인 되어 있고(LoginHelper::isLogin()) 글쓴이라면 $post['author'] == LoginHelper::memberId() 글을 수정/삭제할 권한이 있다고 판단합니다.


이번에는 뷰를 수정해서 수정/삭제 버튼이 보이게 해 보겠습니다.
기존의 컨텐츠 영역 아래에 버튼 부분을 추가합니다.
app/Views/post/show.php

<!-- 신규 추가 영역 시작 -->
<?php if ($isAuthor) : // (1) ?>
<div style="text-align: right;">
    <form method="POST" action="/post/delete"> <!-- (2) -->
        <a href="/post/edit/<?= $post['post_id'] ?>" class="btn btn-info">수정</a>  <!-- (3) -->
        <input type="hidden" name="post_id" value="<?= $post['post_id']?>" />  <!-- (4) -->
        <input type="submit" class="btn btn-danger" value="삭제">  <!-- (5) -->
    </form>
</div>
<?php endif ?>
    <!-- 신규 추가 영역 끝 -->

(1) 글쓴이에게만 보이는 영역을 시작합니다.

(2) 우리는 삭제 엔드포인트 /post/delete 를 HTTP POST 메소드만 받아들이게 작성했습니다. 따라서 <form> 으로 감싸서 HTTP POST로 데이터를 전송하도록 합니다.

(3) 글 수정 링크입니다. 단순 a 태그이므로 폼 값이 서버로 전송되지 않습니다.

(4) input type="hidden" 태그는 사용자에게는 보이지 않지만 프로그램을 위해서는 존재해야 하는 값을 다룰 때 쓰입니다. 주로 예제처럼 POST로 값을 전달하거나 할 때죠. 웹 브라우저 화면에는 아무런 표시도 나오지 않습니다만, <form> 태그 안에 있다면 서버에서는 값을 읽을 수 있습니다.

(5) 서버로 데이터를 전송하는 버튼입니다.


똑같이 조회 페이지 - http://localhost:8080/index.php/post/show/3에 접속했을 때 로그인이 되어 있는 브라우저와 로그인이 되어 있지 않은 브라우저는 서로 다른 화면이 보여집니다.


두 이미지 중 위의 이미지는 로그인이 되어 있고 글 작성자일 때 버튼이 보여지는 것을 나타내고 아래 이미지는 로그인이 되어 있지 않았을 때의 모양을 나타냅니다.

생성 / 수정 페이지에서 취소하기 버튼 추가하기

뷰의 저장 버튼 아래에 취소 버튼을 추가하겠습니다. 생성과 수정은 같은 뷰를 사용하므로 하나만 수정하면 됩니다.
app/Views/post/create.php

<input type="submit" class="btn btn-primary" value="저장">
<a href="/post/" class="btn btn-info">취소</a> <!-- 추가된 코드 -->

목록 페이지에서 로그인 되어 있다면 글쓰기 버튼 보이기

목록 컨트롤러에서 로그인 여부를 뷰로 전달합니다.
app/Controllers/Post.php

return view("post/index", [
    'post_list' => $post_list,
    'pager' => $pager,
    'isLogin' => LoginHelper::isLogin() // 추가된 코드
]);

뷰에 글쓰기 버튼을 추가합니다.
app/Views/post/index.php

<?= $pager->links() ?>
<!-- 추가된 코드 시작 -->
<?php if ($isLogin) : ?>
    <p style="text-align: right;">
        <a href="/post/create" class="btn btn-primary">글쓰기</a>
    </p>
<?php endif ?>
<!-- 추가된 코드 끝 -->
<?= $this->endSection() ?>

레이아웃 페이지에 로그인 / 로그아웃 버튼 추가

원칙적으로는 뷰에서 사용되는 데이터는 모두 컨트롤러에서 전달되어야 합니다. 하지만 레이아웃은 모든 페이지에서 사용되므로, 로그인 여부를 뷰에 전달하기 위해 모든 페이지의 컨트롤러를 수정하는 건 너무 귀찮은 일이죠.
그래서 레이아웃에서는 직접 로그인 여부에 접근하도록 하겠습니다.
기존의 네비게이션 영역은 아래와 같았습니다.
app/Views/post/layout.php

<ul class="navbar-nav flex-column text-left">
    <li class="nav-item active">
        <a class="nav-link" href="index.html"><i class="fas fa-home fa-fw mr-2"></i>Blog Home <span class="sr-only">(current)</span></a>
    </li>
    <li class="nav-item">
        <a class="nav-link" href="blog-post.html"><i class="fas fa-bookmark fa-fw mr-2"></i>Blog Post</a>
    </li>
</ul>

아래와 같이 변경하겠습니다.

<ul class="navbar-nav flex-column text-left">
    <li class="nav-item">
        <?php
        if (App\helpers\LoginHelper::isLogin()) {
            $login_link = "/oauth/logout";
            $login_message = "로그아웃";
        }else{
            $login_link = "/oauth/google";
            $login_message = "로그인";
        }
        ?>
        <a class="nav-link" href="<?= $login_link ?>"><i class="fas fa-bookmark fa-fw mr-2"></i><?= $login_message ?></a>
    </li>
</ul>

이제 네비게이션 링크는 로그인 유무를 나타내도록 바뀌었습니다.

블로그 이름 바꾸고 코드 정리하기

블로그 이름을 바꾸고 링크를 글 목록으로 변경하겠습니다. 기존 코드는 아래와 같습니다.
app/Views/post/layout.php

<h1 class="blog-name pt-lg-4 mb-0"><a href="index.html">Anthony's Blog</a></h1>

이렇게 바꿀께요.

<h1 class="blog-name pt-lg-4 mb-0"><a href="/post">마크다운 블로그</a></h1>

이미지와 설명은 간결함을 위해 제거하겠습니다. 아래의 코드를 삭제해 주세요.
app/Views/post/layout.php

<img class="profile-image mb-3 rounded-circle mx-auto" src="/assets/images/profile.png" alt="image" >

<div class="bio mb-3">Hi, my name is Anthony Doe. Briefly introduce yourself here. You can also provide a link to the about page.<br><a href="about.html">Find out more about me</a></div><!--//bio-->

Get in Touch 영역도 지울께요.
app/Views/post/layout.php

<div class="my-2 my-md-3">
    <a class="btn btn-primary" href="https://themes.3rdwavemedia.com/" target="_blank">Get in Touch</a>
</div>

마지막으로 헤드의 글자를 바꿉시다.
app/Views/post/layout.php

<head>
    <title>Bootstrap 4 Blog Template For Developers</title>

대신

<head>
    <title>마크다운 블로그</title>

로 변경합니다.

이제 우리의 마크다운 블로그는 아래와 같이 보입니다.

반응형으로도 잘 보이는지 확인해 봅시다.
브라우저에서 개발자 도구(F12)를 누른 후 반응형 테스트 버튼을 눌러봅니다.

모바일에서는 이렇게 보입니다.

모바일에서 네비게이션을 펼치면 이렇게 보이게 되죠.

profile
중년 아저씨. 10 + n년차 백엔드 개발자. 스타트업과 창업, 솔로프리너와 1인 기업에 관심 많아요.

2개의 댓글

comment-user-thumbnail
2021년 12월 10일

아! 혹시 로컬서버에서 하는거라서 버그? 가 보인가봐요~ writable 폴더가 있으면 세션 쓸때 권한설정 해야되고
디비에서 생성하는거면 아무이상 없을거에요.. 로컬에선 잔버그가 좀 보이더라구요

1개의 답글