갈루아의 반서재


Luminus 를 이용한 Clojure 방명록 만들기 (4)


Creating Pages and Handling Form Input

guestbook.routes.home 네임스페이스에 경로가 정의되어 있습니다. 그러면 파일을 열어서 데이터베이스로부터 메시지를 렌더링하는 로직을 넣어보겠습니다. Bouncer 검증과 ring.util.response 레퍼런스와 함께 db 네임스페이스 레퍼런스를 추가해야합니다. 

/home/fukaerii/guestbook/src/clj/guestbook/routes/home.clj

1
2
3
4
5
6
7
8
(ns guestbook.routes.home
  (:require [guestbook.layout :as layout]
            [compojure.core :refer [defroutes GET POST]]
            [ring.util.http-response :as response]
            [clojure.java.io :as io]
            [guestbook.db.core :as db]
            [struct.core :as st]))
 
cs

다음으로 폼 매개변수를 정의하는 스키마를 생성하고, 이를 검증하는 함수를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
(def message-schema
  [[:name st/required st/string]
   [:message
    st/required
    st/string
    {:message "message must contain at least 10 characters"
     :validate #(> (count %) 9)}]])
 
(defn validate-message [params]
  (first
    (st/validate params message-schema)))
cs

:name 과 :message 키가 우리가 정의한 규칙에 부합하는지 확인하기 위해 Struct 라이브러리로부터 검증 함수를 가져와 사용합니다. 특히 이름은 최소 10자 이상이어야 합니다. 그러면 메시지를 검증하고 저장하는 함수를 추가해보겠습니다.

1
2
3
4
5
6
7
8
(defn save-message! [{:keys [params]}]
  (if-let [errors (validate-message params)]
    (-> (response/found "/")
        (assoc :flash (assoc params :errors errors)))
    (do
      (db/save-message!
        (assoc params :timestamp (java.util.Date.)))
      (response/found "/"))))
cs

폼 매개변수를 포함한 요청으로부터 :params 키를 뽑아냅니다. validate-message 함수가 에러를 반환해내면 / 로 리다이렉트 되도록 정의합니다. 홈페이지 핸들러 함수를 다음과 같이 변경합니다.

[기존]

1
2
3
(defn home-page []
  (layout/render
    "home.html" {:docs (-> "docs/docs.md" io/resource slurp)}))
cs

[변경]

1
2
3
4
5
(defn home-page [{:keys [flash]}]
  (layout/render
    "home.html"
    (merge {:messages (db/get-messages)}
           (select-keys flash [:name :message :errors]))))
cs

홈페이지 템플릿을 렌더링하고, 예를 들어, 검증 에러 등 :flash 세션으로부터 파라메터와 함께 현재 저장된 메시지에 전달합니다. 

guestbook.db.core 네임스페이스에 있는 (conman/bind-connection *db* "sql/queries.sql") 명령을 통해 데이터베이스 접근 함수가 자동으로 생성되었음을 기억하시기 바랍니다. 이러한 함수의 명칭은 resources/sq/queries.sql 파일의 SQL 템플릿의 :name 코멘트로부터 추론이 가능합니다. 

1
2
3
4
5
(defroutes home-routes
  (GET "/" request (home-page request))
  (POST "/" request (save-message! request))
  (GET "/about" [] (about-page)))
 
cs

compojure.core 으로부터 POST 를 참조하는 것을 잊지 마세요.

1
2
3
4
5
6
7
(ns guestbook.routes.home
  (:require [guestbook.layout :as layout]
            [compojure.core :refer [defroutes GET POST]]
            [ring.util.http-response :as response]
            [clojure.java.io :as io]
            [guestbook.db.core :as db]
            [struct.core :as st]))
cs

이제 컨트롤러 셋업을 해봅니다. resources/html 디렉토리에 위치한 home.html 템플릿을 열어봅시다. 지금은 단순하게 content 블록 안에서 content 변수의 내용만 렌더링하고 있습니다. 

/home/fukaerii/guestbook/resources/html/home.html

1
2
3
4
5
6
7
8
{% extends "base.html" %}
{% block content %}
<div class="row">
  <div class="col-sm-12">
    {{docs|markdown}}
  </div>
</div>
{% endblock %}
cs

메시지를 반복해서 출력할 수 있도록 content 블록을 다음과 같이 수정합니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{% extends "base.html" %}
{% block content %}
<div class="row">
    <div class="span12">
        <ul class="messages">
            {% for item in messages %}
            <li>
                <time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
                <p>{{item.message}}</p>
                <p> - {{item.name}}</p>
            </li>
            {% endfor %}
        </ul>
    </div>
</div>
{% endblock %}
cs

위에서 보는 바와 같이 for 반복자를 사용했습니다. 각각의 메시지는 메시지, 이름, 그리고 타임스탬프 키와 함께 맵으로 구성되어 있기 때문에, 이름으로 접근이 가능합니다. 

마지막으로 사용자가 메시지를 입력할 수 있도록 폼을 만들어 봅시다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<div class="row">
    <div class="span12">
        <form method="POST" action="/">
                {% csrf-field %}
                <p>
                    Name:
                    <input class="form-control"
                           type="text"
                           name="name"
                           value="{{name}}" />
                </p>
                {% if errors.name %}
                <div class="alert alert-danger">{{errors.name|join}}</div>
                {% endif %}
                <p>
                    Message:
                <textarea class="form-control"
                          rows="4"
                          cols="50"
                          name="message">{{message}}</textarea>
                </p>
                {% if errors.message %}
                <div class="alert alert-danger">{{errors.message|join}}</div>
                {% endif %}
                <input type="submit" class="btn btn-primary" value="comment" />
        </form>
    </div>
</div>
cs

최종 home.html 템플릿의 모양은 다음과 같습니다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
{% extends "base.html" %}
{% block content %}
<div class="row">
    <div class="span12">
        <ul class="messages">
            {% for item in messages %}
            <li>
                <time>{{item.timestamp|date:"yyyy-MM-dd HH:mm"}}</time>
                <p>{{item.message}}</p>
                <p> - {{item.name}}</p>
            </li>
            {% endfor %}
        </ul>
    </div>
</div>
<div class="row">
    <div class="span12">
        <form method="POST" action="/">
                {% csrf-field %}
                <p>
                    Name:
                    <input class="form-control"
                           type="text"
                           name="name"
                           value="{{name}}" />
                </p>
                {% if errors.name %}
                <div class="alert alert-danger">{{errors.name|join}}</div>
                {% endif %}
                <p>
                    Message:
                <textarea class="form-control"
                          rows="4"
                          cols="50"
                          name="message">{{message}}</textarea>
                </p>
                {% if errors.message %}
                <div class="alert alert-danger">{{errors.message|join}}</div>
                {% endif %}
                <input type="submit" class="btn btn-primary" value="comment" />
        </form>
    </div>
</div>
{% endblock %}
cs

마지막으로 resources/public/css 폴더 아래의 screen.css 파일을 업데이트합니다.

/home/fukaerii/guestbook/resources/public/css/screen.css

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
html,
body {
    font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
    height: 100%;
}
.navbar {
  margin-bottom: 10px;  
  border-radius: 0px;
}
.navbar-brand {
  float: none;
}
.navbar-nav .nav-item {
  float: none;
}
.navbar-divider,
.navbar-nav .nav-item+.nav-item,
.navbar-nav .nav-link + .nav-link {
  margin-left: 0;
}
@media (min-width: 34em) {
  .navbar-brand {
    float: left;
  }
  .navbar-nav .nav-item {
    float: left;
  }
  .navbar-divider,
  .navbar-nav .nav-item+.nav-item,
  .navbar-nav .nav-link + .nav-link {
    margin-left: 1rem;
  }
}
cs