· nambak80 Blog 로그인
CodeIgniter4

CodeIgniter 4로 블로그 만들기 #20 — 댓글 폼과 인증 가드

CodeIgniter 4로 블로그 만들기 #20 — 댓글 폼과 인증 가드

#지난 회차에서 댓글 저장 백엔드를 테스트 우선으로 완성했습니다. 하지만 아직 브라우저에는 댓글을 입력할 폼이 없습니다. 이번 회차에서 그 폼을 붙입니다.

핵심은 단순히 폼을 그리는 게 아니라, 로그인한 사용자에게만 폼을 노출하고, 비로그인 사용자에게는 로그인 유도를 보여 주는 것입니다. 그리고 이 "폼 노출 가드"는 실무에서 흔히 빠뜨리는 부분이라, 테스트로 못 박고 넘어갑니다.

왜 저장 라우트만 막으면 안 되나

ep19에서 저장 라우트(POST /posts/{id}/comments)를 session 필터로 이미 막았습니다. 그러면 폼 노출까지 신경 쓸 필요가 있을까요?

있습니다. 두 가지는 역할이 다릅니다.

  • 저장 라우트 가드(ep19): 비로그인 사용자가 어떻게든 POST를 보내도 서버가 막는다 → 보안.
  • 폼 노출 가드(이번 회차): 비로그인 사용자에게 애초에 쓸 수 없는 폼을 보여 주지 않는다 → UX.

폼을 보여 놓고 제출하면 로그인 페이지로 튕기는 건 사용자 입장에서 당황스럽습니다. "로그인해야 쓸 수 있다"는 걸 폼 대신 미리 안내하는 게 좋습니다. 그래서 이 UX 규칙을 코드뿐 아니라 테스트로도 고정합니다.

준비물과 목표

  • ep19까지 완성된 저장소 (댓글 저장 백엔드가 동작하는 상태)
  • ep11에서 익힌 auth()->loggedIn() 뷰 분기

이번에 만들 것:

  1. CommentGuardTest — 비로그인엔 폼 미노출 / 로그인엔 폼 노출 (실패 → 통과)
  2. comments/_form.php 부분 뷰
  3. posts/show.php — 로그인 시 폼 include, 비로그인은 로그인 안내

1. 먼저 실패하는 테스트 (빨강)

이번에도 테스트를 먼저 씁니다. tests/Feature/CommentGuardTest.php입니다.

<?php

namespace Tests\Feature;

use App\Models\PostModel;
use CodeIgniter\Shield\Entities\User;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use CodeIgniter\Test\FeatureTestTrait;
use CodeIgniter\Shield\Test\AuthenticationTesting;

/**
 * 댓글 폼의 인증 가드에 대한 Feature 테스트.
 *
 * 댓글 폼은 로그인 사용자에게만 보이고, 비로그인 사용자에게는 보이지 않는다.
 * (저장 라우트 자체의 차단은 CommentStoreTest 에서 검증한다.)
 */
final class CommentGuardTest extends CIUnitTestCase
{
    use DatabaseTestTrait;
    use FeatureTestTrait;
    use AuthenticationTesting;

    protected $namespace = null;
    protected $refresh   = true;

    protected function setUp(): void
    {
        parent::setUp();

        $_SESSION = [];
        \Config\Services::resetSingle('session');
        \Config\Services::resetSingle('auth');
    }
    // ... makeUser(), makePostSlug() 헬퍼 (이전과 동일한 패턴) ...
}

트레이트 구성과 setUp()의 세션 리셋은 이전의 CommentStoreTest와 같은 패턴입니다. 이번엔 상세 페이지를 GET으로 불러 화면에 폼이 있는지를 봐야 하므로, 글의 slug가 필요합니다.

private function makePostSlug(int $userId): string
{
    $model = model(PostModel::class);
    $model->insert([
        'user_id' => $userId,
        'title'   => '댓글 폼 테스트 글',
        'body'    => '본문',
    ]);

    return $model->find($model->getInsertID())->slug;
}

그리고 두 시나리오입니다.

public function testGuestDoesNotSeeCommentForm(): void
{
    $slug = $this->makePostSlug(1);

    $result = $this->call('GET', 'posts/' . $slug);

    $result->assertStatus(200);
    $result->assertDontSee('댓글 등록');
}

public function testLoggedInUserSeesCommentForm(): void
{
    $user = $this->makeUser();
    $slug = $this->makePostSlug($user->id);

    $result = $this->actingAs($user)->call('GET', 'posts/' . $slug);

    $result->assertStatus(200);
    $result->assertSee('댓글 등록');
}

포인트:

  • assertDontSee('댓글 등록') / assertSee('댓글 등록') — 폼의 제출 버튼 라벨("댓글 등록")이 응답 HTML에 있는지로 폼 노출 여부를 판정합니다. 눈에 보이는 문자열을 기준 삼으면 테스트가 읽기 쉽습니다.
  • 비로그인은 actingAs 없이 GET, 로그인은 actingAs($user)로 GET. 두 경우 모두 200(상세 페이지 자체는 누구나 볼 수 있음)이지만, 폼 노출만 달라야 합니다.

이 시점엔 폼이 아예 없으니 testLoggedInUserSeesCommentForm이 빨강입니다.

2. 댓글 폼 부분 뷰

로그인 사용자에게 보여 줄 폼을 부분 뷰로 만듭니다. comments/_form.php입니다.

<?php // 댓글 작성 폼 부분 뷰. $post 를 받는다(action URL 용). 로그인 시에만 노출. ?>
<form class="comment-form" action="<?= site_url('posts/' . $post->id . '/comments') ?>" method="post">
    <?= csrf_field() ?>

    <?php $errors = session('errors') ?? []; ?>
    <?php if ($errors !== []): ?>
        <ul class="form-errors">
            <?php foreach ($errors as $error): ?>
                <li><?= esc($error) ?></li>
            <?php endforeach ?>
        </ul>
    <?php endif ?>

    <textarea name="body" rows="3" placeholder="댓글을 남겨 보세요"><?= esc(old('body')) ?></textarea>
    <button type="submit" class="btn">댓글 등록</button>
</form>

읽어 볼 지점:

  • actionposts/{post->id}/comments. ep19에서 session 그룹에 등록한 (:num) 라우트에 맞춰 글 ID를 씁니다.
  • csrf_field() — CI4의 CSRF 보호. 쓰기 폼에는 반드시 넣습니다(이전부터 이어 온 습관).
  • 에러 표시session('errors')는 이전 컨트롤러가 검증 실패 시 with('errors', ...)로 넘긴 값입니다. 빈 내용을 제출하면 여기에 "댓글 내용을 입력해 주세요."가 찍혀 폼 위에 나타납니다.
  • old('body') — 역시 ep19의 withInput() 덕분에, 검증 실패로 되돌아왔을 때 방금 친 내용이 textarea에 그대로 남습니다. 사용자가 다시 타이핑하지 않아도 됩니다. esc()로 감싸 XSS도 막습니다.

폼 스타일도 app.css에 조금 더합니다.

.comment-form { margin-top: 24px; }
.comment-form textarea { margin-bottom: 10px; }
.comment-login { margin-top: 24px; color: var(--color-ink-3); }

3. 상세 뷰에서 로그인 분기

이제 상세 뷰(posts/show.php)에서 댓글 목록 아래에 로그인 여부로 분기합니다.

    <?php // 댓글 목록(부분 뷰). ?>
    <?= $this->include('comments/_list', ['comments' => $comments]) ?>

    <?php // 댓글 작성 폼은 로그인 사용자에게만 노출한다(비로그인은 로그인 유도). ?>
    <?php if (auth()->loggedIn()): ?>
        <?= $this->include('comments/_form', ['post' => $post]) ?>
    <?php else: ?>
        <p class="comment-login"><a class="nav-link" href="<?= site_url('login') ?>">로그인</a> 후 댓글을 남길 수 있습니다.</p>
    <?php endif ?>

auth()->loggedIn()(이전에 헤더 분기에 이미 쓴 그 함수)으로 갈라, 로그인 상태면 폼 부분 뷰를 include하고, 아니면 로그인 링크가 담긴 안내 문구를 보여 줍니다. 폼 부분 뷰에는 action URL을 만들기 위해 $post를 넘깁니다.

이렇게 하면:

  • 비로그인: "댓글 등록" 버튼이 렌더링되지 않음 → assertDontSee('댓글 등록') 통과.
  • 로그인: 폼이 렌더링되어 "댓글 등록" 버튼 등장 → assertSee('댓글 등록') 통과.

확인 — 초록, 그리고 브라우저

테스트를 다시 돌립니다.

composer test -- --filter CommentGuardTest

두 테스트가 초록으로 바뀌면, 브라우저에서도 확인해 봅니다.

php spark serve
  • 로그아웃 상태로 글 상세에 가면 댓글 목록 아래에 "로그인 후 댓글을 남길 수 있습니다."가 보입니다.
  • 로그인하면 그 자리에 textarea와 "댓글 등록" 버튼이 나타납니다. 실제로 댓글을 써서 등록하면(ep19의 저장 로직이 받습니다) slug 페이지로 돌아오며 방금 단 댓글이 목록에 추가됩니다.
  • 내용을 비우고 등록하면 "댓글 내용을 입력해 주세요."가 폼 위에 뜨고, 입력값도 유지됩니다.

여기까지 오면 표시 → 저장 → 폼·가드 가 하나로 이어집니다.

마무리 — 커밋과 태그

이번 회차에서 한 일:

  • CommentGuardTest — 비로그인 폼 미노출 / 로그인 폼 노출(실패 → 통과)
  • comments/_form.php 부분 뷰(CSRF·에러·old() 복원) + 폼 스타일
  • posts/show.php — 로그인 시 폼 include, 비로그인은 로그인 안내
git add .
git commit -m "feat: 댓글 폼과 인증 가드"
git tag ep20

다음 회차

이제 댓글을 달 수 있으니, 마지막으로 지울 수 있어야 합니다. 다음 글(#21)에서는 댓글 삭제와 권한을 다룹니다. "댓글 작성자 본인 · 글 작성자 · 관리자만 삭제"라는 권한 조합을, 컨트롤러 가드와 화면 노출 양쪽에서 맞추고, 제3자 차단(403)까지 테스트로 못 박습니다.


이번 회차 요약

  • 다루는 파일: tests/Feature/CommentGuardTest.php·app/Views/comments/_form.php(생성) / app/Views/posts/show.php·public/assets/css/app.css(수정)
  • 핵심: 저장 라우트 가드(보안)와 폼 노출 가드(UX)는 별개다. auth()->loggedIn()으로 폼을 로그인 사용자에게만 노출하고, 이 규칙을 assertSee/assertDontSee로 고정한다. 폼은 CSRF·old()로 실패 복원까지.

다음: 댓글 삭제와 권한(본인/글작성자/관리자)

댓글 0

아직 댓글이 없습니다.

로그인 후 댓글을 남길 수 있습니다.

← 목록으로