-
Notifications
You must be signed in to change notification settings - Fork 12
/
Copy path07-creating-posts.md.erb
483 lines (355 loc) · 22.5 KB
/
07-creating-posts.md.erb
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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
---
title: Post 등록하기
slug: creating-posts
date: 0007/01/01
number: 7
contents: 클라이언트에서 Post를 등록하는 방법을 배운다.|간단한 보안성 검사를 구현한다.|Post 등록폼에 대한 접근을 제한한다.|추가보안을 위해 서버쪽 메서드 사용법을 배운다.
paragraphs: 60
---
Posts.insert 데이터베이스 호출을 사용하여 콘솔에서 post를 등록하는 것이 매우 쉽다는 것은 알지만, 사용자가 post를 등록하기 위해 콘솔을 열도록 할 수는 없다!
결국, 우리는 사용자가 앱에 새 글을 등록하는 사용자 인터페이스를 구현해야 한다.
### Post 등록 페이지 만들기
먼저 새 페이지에 대한 route를 정의한다:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "15" %>
### 헤더에 링크 추가하기
Route를 정의했으니, 이제 헤더에 등록 페이지에 대한 링크를 추가할 수 있다:
~~~html
<template name="header">
<nav class="navbar navbar-default" role="navigation">
<div class="container-fluid">
<div class="navbar-header">
<button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#navigation">
<span class="sr-only">Toggle navigation</span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</button>
<a class="navbar-brand" href="{{pathFor 'postsList'}}">Microscope</a>
</div>
<div class="collapse navbar-collapse" id="navigation">
<ul class="nav navbar-nav">
<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>
</ul>
<ul class="nav navbar-nav navbar-right">
{{> loginButtons}}
</ul>
</div>
</div>
</nav>
</template>
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "14~16" %>
Route를 설정했다는 것은 사용자가 브라우저에서 `/submit` URL을 요청하면, 미티어가 `postSubmit` 템플릿을 화면에 보여준다는 의미이다. 그러므로 아래와 같이 템플릿을 작성한다:
~~~html
<template name="postSubmit">
<form class="main form">
<div class="form-group">
<label class="control-label" for="url">URL</label>
<div class="controls">
<input name="url" id="url" type="text" value="" placeholder="Your URL" class="form-control"/>
</div>
</div>
<div class="form-group">
<label class="control-label" for="title">Title</label>
<div class="controls">
<input name="title" id="title" type="text" value="" placeholder="Name your post" class="form-control"/>
</div>
</div>
<input type="submit" value="Submit" class="btn btn-primary"/>
</form>
</template>
~~~
<%= caption "client/templates/posts/post_submit.html" %>
주: 많은 마크업이 있지만, 이들은 단순히 Twitter Bootstrap에서 온 것이다. form 엘리먼트들만이 필수적이고, 나머지 마크업은 앱을 다소 보기좋게 해 줄 뿐이다. 이것은 아래와 같이 보일 것이다:
<%= screenshot "7-1", "Post 등록폼" %>
이것은 단순한 폼이다. 이 폼의 작동여부에 대하여는 걱정할 필요없다. 우리는 폼의 submit 이벤트를 가로채어 Javascript를 통해서 데이터를 갱신할 것이다(Javascript가 비활성화된 경우, 미티어 앱이 전혀 동작하지 않는 경우를 고려하여, JS가 없을 때의 대체 페이지를 제공하는 것은 별 의미는 없다.).
### Post 등록하기
폼의 `submit` 이벤트를 이벤트 핸들러에 묶는다. 이 때, `submit` 이벤트를 사용하는 것이 최선(버튼의 `click` 이벤트를 사용하는 것에 비하여)인데, 이는 가능한 모든 submit 요청(예를 들면, URL 필드에서 엔터 키를 눌렀을 때와 같이)를 모두 커버하여 처리하기 때문이다.
~~~js
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
post._id = Posts.insert(post);
Router.go('postPage', post);
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= commit "7-1", "Post 등록 페이지를 추가하고 헤더에 이 페이지로의 링크를 추가했다." %>
이 함수는 [jQuery](http://jquery.com)를 사용하여 폼 필드의 값들을 추출하고, 그 결과로부터 새로운 post 객체를 구성한다. 여기서 핸들러의 매개변수 `event`의 `preventDefault` 메서드를 호출하여 브러우저가 폼의 submit을 그대로 진행하지 않도록 차단해야 한다.
마침내, 우리는 새로운 post 페이지로 route를 설정할 수 있다. 컬렉션의 `insert()` 함수는 데이터베이스에 저장되는 객체의 `id`를 리턴한다. 이 과정에서 라우터의 `go()` 함수를 통해서 이동할 URL로 간다.
결론은 사용자가 submit버튼을 누르면, post가 생성, 등록되고, 사용자는 이 등록된 post에 대한 토론 페이지로 바로 가는 것이다.
### 추가 보안
Post를 등록하는 과정은 잘 되지만, 모든 방문자들이 다 등록하게 하려는 것은 아니다: 우리는 방문자들이 등록하려면 로그인하게 하려고 한다. 물론, 로그아웃 상태의 사용자에게는 post 등록 폼을 숨기게 할 수도 있다. 하지만 아직은, 사용자는 로그인하지 않고도 브라우저 콘솔에서 post를 등록할 수 있으니, 이대로 둘 수는 없다.
감사하게도 데이터 보안 기능이 미티어 컬렉션에 잘 구현되어 있다; 이 기능은 새 프로젝트를 만들때, 기본적으로 꺼진 상태로 되어 있다. 그래서 쉽게 시작할 수 있었고, 지루한 작업은 나중으로 미루고 앱을 구축할 수 있었다.
우리 앱은 이제 더 이상 이런 보호막이 없어도 되는 상태가 되었으니, 이를 벗어 버리도록 하자! 이제 `insecure` 패키지를 제거한다:
~~~bash
$ meteor remove insecure
~~~
<%= caption "터미널" %>
이렇게 하면, post 등록 폼이 더 이상 동작하지 않는 것을 볼 수 있다. 이것은 `insecure` 패키지가 없으면, 클라이언트 쪽에서 post 컬렉션에 등록하는 것이 _더 이상 허용되지 않기_ 때문이다. 클라이언트 쪽에서 post 등록을 가능하게 하려면 미티어에게 명시적인 규정을 부여하거나, 그렇지 않으면 서버쪽에서 post를 등록해야 한다.
### Post 등록을 허용하기
등록폼이 다시 동작하도록 클라이언트 쪽에서 post 등록을 허용하는 방법을 알아보자. 나중에 알게되지만, 우리는 결국에는 다른 기술을 사용할 것이다. 하지만, 지금은 아래 방법으로 하면 쉽게 동작하게 할 수 있다:
~~~js
Posts = new Mongo.Collection('posts');
Posts.allow({
insert: function(userId, doc) {
// only allow posting if you are logged in
return !! userId;
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~8" %>
<%= commit "7-2", "패키지 insecure를 삭제하고 post에 쓰기를 허용했다." %>
우리는 `Posts.allow`를 호출한다. 이것은 미티어에게 "다음 조건하에서 클라이언트의 Posts 컬렉션 조작을 허용한다."라고 지시하는 것이다. 위의 경우, 우리는 "클라이언트가 `userId`를 가지고 있으면 post를 등록하도록 허용한다" 라고 지시하는 것이다.
수정 작업을 하려는 사용자의 `userId`는 `allow`와 `deny` 요청에 전달된다(혹은 로그인 사용자가 없으면 `null`을 리턴한다). 그리고 사용자 계정이 미티어의 핵심 부분과 묶여있으니 항상 정확한 `userId`에 의존할 수 있다.
우리는 post를 등록하려면 로그인하도록 해왔다. 로그아웃한 다음 post를 등록하려고 시도해보기 바란다; 그러면 콘솔에서 이런 메시지를 볼 것이다:
<%= screenshot "7-2", "등록 실패: Access denied " %>
그런데, 우리는 여전히 다음의 이슈들을 처리해야 한다:
- 로그아웃 상태의 사용자가 여전히 post 등록 폼 페이지에 접근할 수 있다.
- Post가 그 사용자와 어떤 방식으로든 묶여있지 않다(그리고, 이를 강제할 코드가 서버에는 없다).
- 동일한 URL을 가리키는 복수의 post가 등록될 수 있다.
이런 문제들의 해법을 찾아보자.
### 새 Post 등록폼에 접근 제한하기
로그아웃 상태의 사용자가 post 등록폼을 열람하는 것을 막는 것으로 시작하자. 이 작업은 *route hook*을 정의하는 방식으로 라우터 레벨에서 할 것이다.
Hook이란 라우팅 과정을 가로채서 라우터가 수행하는 작업을 바꾸어 버린다. 이것은 경호원이 당신의 입장을 허용(또는 돌려보내는 것)하기 전에 당신의 신원을 확인하는 과정과 대비하여 생각할 수 있다.
우리가 해야 할 일은 사용자가 로그인 했는지를 검사하는 것이다. 로그인하지 않았다면, `postSubmit` 템플릿 대신에 `accessDenied` 템플릿을 그린다(그리고는 라우터의 동작을 중지한다). 그러므로, router.js 파일을 아래와 같이 수정한다:
~~~js
Router.configure({
layoutTemplate: 'layout',
loadingTemplate: 'loading',
notFoundTemplate: 'notFound',
waitOn: function() { return Meteor.subscribe('posts'); }
});
Router.route('/', {name: 'postsList'});
Router.route('/posts/:_id', {
name: 'postPage',
data: function() { return Posts.findOne(this.params._id); }
});
Router.route('/submit', {name: 'postSubmit'});
var requireLogin = function() {
if (! Meteor.user()) {
this.render('accessDenied');
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "17~23,26" %>
접근 거부 페이지를 위한 템플릿도 만든다:
~~~html
<template name="accessDenied">
<div class="access-denied jumbotron">
<h2>Access Denied</h2>
<p>You can't get here! Please log in.</p>
</div>
</template>
~~~
<%= caption "client/templates/includes/access_denied.html" %>
<%= commit "7-3", "로그인 상태가 아닐 때 post 등록 페이지로의 접근을 거절했다." %>
이제 로그인하지 않은 상태로 http://localhost:3000/submit/ URL에 접근 요청을 하면 다음과 같은 장면을 보게 된다:
<%= screenshot "7-3", "Access denied 템플릿" %>
이러한 라우팅 hook의 좋은 점은 이것도 *반응형*이라는 점이다. 이 의미는 사용자가 로그인할 때, 콜백이나 그 비슷한 것을 생각할 필요가 없다는 것이다. 사용자의 로그인 상태가 변할 때, 라우터의 페이지 템플릿은 `accessDenied`에서 `postSubmit`로 순간적으로 변하는 데, 이 과정에 이를 다루는 어떤 명시적 코드도 작성할 필요가 없다 (그리고, 이것은 다른 브라우저 탭에서도 일어난다).
로그인한 다음, 해당 페이지를 새로고침해보자. 접근거절 템플릿이 짧은 순간에 새 글쓰기 페이지로 바뀌어 나타나는 것을 보게 될 것이다. 이렇게 되는 이유는 미티어가 템플릿 렌더링을 가능한 신속하게 수행하기 때문인데, 이 시점은 서버와 통신하기 전이며, 해당 사용자가 현재(브라우저의 로컬 저장소에 저장되어) 존재하는 지를 체크하기도 전이다.
이 문제(이것은 클라이언트와 서버 사이의 대기 시간에 대한 복잡한 문제를 처리할 때면 자주 겪는 일상적인 종류의 문제이기도 하다)를 해결하기 위해서, 우리는 사용자가 접속한 상태인지를 알아보기 위해 기다리는 짧은 시간 동안 구동화면을 보여줄 것이다.
결국 이 시점에서 우리는 사용자가 정확한 로그인 인증 정보를 가지고 있는지를 모르며, 우리가 이를 확인할 때까지 우리는 `accessDenied`나 `postSubmit` 템플릿 중 어느 것을 보여줄 것인지를 결정할 수 없다.
그래서 우리는 hook 처리 부분을 변경하여, `Meteor.loggingIn()`이 `true`인 동안 로딩중 템플릿을 사용한다:
~~~js
//...
var requireLogin = function() {
if (! Meteor.user()) {
if (Meteor.loggingIn()) {
this.render(this.loadingTemplate);
} else {
this.render('accessDenied');
}
} else {
this.next();
}
}
Router.onBeforeAction('dataNotFound', {only: 'postPage'});
Router.onBeforeAction(requireLogin, {only: 'postSubmit'});
~~~
<%= caption "lib/router.js" %>
<%= highlight "5~10" %>
<%= commit "7-4", "로그인을 기다리는 동안 로딩중 화면을 보인다." %>
### 링크 숨기기
사용자가 로그아웃 상태에서 이 페이지에 실수로 접근하는 것을 방지하는 가장 쉬운 방법은 해당 링크를 숨기는 것이다. 이것은 매우 쉽다:
~~~html
//...
<ul class="nav navbar-nav">
{{#if currentUser}}<li><a href="{{pathFor 'postSubmit'}}">Submit Post</a></li>{{/if}}
</ul>
//...
~~~
<%= caption "client/templates/includes/header.html" %>
<%= highlight "3~5" %>
<%= commit "7-5", "로그인 상태인 경우만 post 등록 링크를 보인다." %>
`currentUser` 헬퍼는 accounts 패키지에서 제공하며 `Meteor.user()`와 동등한 handlebars이다. 이것은 반응형이므로 이 링크는 로그인 여부에 따라서 보여지거나 숨겨질 것이다.
### 미티어 메서드(Method): 더 나은 추상화와 보안
이제 로그아웃 상태의 사용자의 새 post 등록에 대한 접근을 막았다. 그리고 그들이 속임수를 쓰거나 콘솔을 통해서 글쓰기를 시도하는 것까지 차단했다. 하지만, 아직도 우리가 다루어야 할 몇 가지가 있다:
- Post에 등록일시를 기록하기.
- 동일한 URL로 동시에 두 번 이상 글쓰기 시도를 차단할 것.
- Post의 저자에 대한 상세정보(ID, username, 등)를 추가하는 것.
이 모든 작업을 `submit` 이벤트 핸들러에서 처리할 수 있다고 생각할 지 모르겠다. 그런데, 실제로 이를 해보면 여러 가지 문제가 발생하는 것을 겪게 된다.
- 등록일시의 경우, 사용자 컴퓨터의 시간이 정확하다는 것을 전제로 해야 하지만, 이것이 항상 그렇다고 보장할 수 없다.
- 클라이언트들은 그 사이트로 등록되는 _모든_ URL에 대하여 알지 못한다. 그들은 그들이 현재 볼 수 있는(이 부분이 얼마나 정확하게 작동하는 지는 나중에 볼 것이다) post들에 대하여만 알기 때문에, 클라이언트 쪽에서 URL의 유일성을 강제할 방법은 없다.
- 마지막으로, 우리가 클라이언트 쪽에서 사용자 정보를 _추가할 수는 있지만_, 그 정확성을 강제할 수는 없다. 사용자 중에는 브라우저 콘솔을 사용하는 사람들도 있기 때문이다.
이런 이유로, 이벤트 핸들러를 단순하게 하는 것이 바람직하며, 컬렉션에 가장 기본적인 삽입이나 수정하는 것 이상의 무엇을 한다면 **메서드(Method)**를 사용하도록 한다.
미티어 메서드는 클라이언트에서 호출하는 서버쪽 함수이다. 이것은 매우 낯설다 - 사실은, 보이지는 않았지만, `Collection`의 `insert`, `update` 그리고 `remove` 함수는 모두 메서드이다. 이들을 생성하는 방법을 알아보자.
`post_submit.js`로 돌아가보자. Posts 컬렉션에 직접 삽입하지 않고, post라는 이름의 메서드를 호출한다:
~~~js
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "10~16" %>
`Meteor.call` 함수의 첫 매개변수에는 메서드 이름을 넣는다. 이 함수를 호출할 때, 여러 매개변수를 전달(이 경우, 입력 폼에서 구성한 `post`객체)할 수 있다. 그리고 마지막에 콜백함수를 추가하는데 이 함수는 서버쪽의 메서드가 수행된 다음에 실행된다.
Meteor method 콜백은 항상 2개의 매개변수 `error`와 `result`를 가진다. 어떤 이유로든 `error` 매개변수가 있으면, 사용자에게 (콜백을 취소하기 위하여 `return`을 통해서) 경고를 보낸다. 만약 모든 것이 잘 돌아가면, 사용자를 새로 만든 post 토론 페이지로 redirect한다.
### 보안 검사
이 기회에 우리는 [`audit-argument-checks`](http://docs.meteor.com/#auditargumentchecks) 패키지를 이용하여 method에 보안을 추가하려고 한다.
이 패키지는 미리 정의된 패턴에 따라서 JavaScript 객체를 검사한다. 예제에서 우리는 이를 이용하여 method를 호출하는 사용자가 제대로 로그인된 상태인지를 (`Meteor.userId()`가 `String`인지를 확인하여) 검사한다. 그리고 매개변수로 전달되는 `postAttributes` 객체의 `title`과 `url`이 문자열인지도 검사한다. 그래서 데이터베이스에 아무 데이터나 들어가지 않도록 한다.
이제 `lib/collections/posts.js` 파일에 `postInsert` 메서드를 정의하자. `posts.js`에서 `allow()` 블럭은 삭제하는데, 이것은 Meteor Method는 이것들을 건너뛰기 때문이다.
그 다음엔 `postAttributes` 객체를 `확장`하여 3개의 속성값을 추가한다: 사용자의 `_id`, `username`, 그리고 post가 `등록`된 시간이다.
이것을 데이터베이스이 입력하여 리턴되는 `_id`값을 JavaScript 객체 형태로 리턴하여 클라이언트( 이 method를 호출한 사용자)에 전달한다.
~~~js
Posts = new Mongo.Collection('posts');
Meteor.methods({
postInsert: function(postAttributes) {
check(Meteor.userId(), String);
check(postAttributes, {
title: String,
url: String
});
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "3~24" %>
`_.extend()`는 [Underscore](http://underscorejs.org) 라이브러리의 method로서, 단순히 하나의 객체에 속성을 추가하여 “확장”하는 기능을 한다.
<%= commit "7-6", "메서드를 사용하여 post를 등록한다." %>
<% note do %>
### Allow/Deny여 바이 바이
Meteor Method는 서버에서 실행된다. 그러므로 Meteor는 이것을 신뢰할 수 있다고 본다. 그래서 Meteor method는 allow/deny 콜백을 건너뛴다.
*서버에서도* 모든 `insert`, `update`, 또는 `remove` 앞에 임의의 코드를 실행시키고 싶다면, [collection-hooks](https://github.com/matb33/meteor-collection-hooks) 패키지를 검토해보길 권한다.
<% end %>
### 이중 등록 방지
Method를 마무리하기 전에 한 가지만 더 지적하고자 한다. 어떤 post가 이미 등록된 것과 같은 URL을 가지는 경우, 우리는 이를 추가하지 않고 대신에 기존의 post로 사용자를 redirect할 것이다.
~~~js
Meteor.methods({
postInsert: function(postAttributes) {
check(this.userId, String);
check(postAttributes, {
title: String,
url: String
});
var postWithSameLink = Posts.findOne({url: postAttributes.url});
if (postWithSameLink) {
return {
postExists: true,
_id: postWithSameLink._id
}
}
var user = Meteor.user();
var post = _.extend(postAttributes, {
userId: user._id,
author: user.username,
submitted: new Date()
});
var postId = Posts.insert(post);
return {
_id: postId
};
}
});
~~~
<%= caption "lib/collections/posts.js" %>
<%= highlight "9~15" %>
데이터베이스에서 동일한 URL을 가지는 post를 검색한다. 하나라도 발견되면, 해당 post의 `_id`와 함께 `postExists: true` 플래그를 추가하여 `리턴`함으로써 클라이언트가 이 특별한 상황을 알도록 한다.
그리고 `return`을 호출하였으므로, method는 `insert` 문을 실행하지 않고 우아하게 이중 등록을 방지하면서 실행을 마치게 된다.
이제 남은 할 일은 `postExists` 정보를 사용하여 클라이언트 쪽에서 이벤트 핸들러를 통해서 경고 메시지를 보여주는 것이다:
~~~js
Template.postSubmit.events({
'submit form': function(e) {
e.preventDefault();
var post = {
url: $(e.target).find('[name=url]').val(),
title: $(e.target).find('[name=title]').val()
};
Meteor.call('postInsert', post, function(error, result) {
// display the error to the user and abort
if (error)
return alert(error.reason);
// show this result but route anyway
if (result.postExists)
alert('This link has already been posted');
Router.go('postPage', {_id: result._id});
});
}
});
~~~
<%= caption "client/templates/posts/post_submit.js" %>
<%= highlight "15~17" %>
<%= commit "7-7", "post URL의 유일함을 구현한다." %>
### Post 정렬
Post의 등록 일시가 있으므로, 이 속성을 이용하여 정렬을 구현할 수 있다. 이를 위해서, 우리는 Mongo의 `sort` 연산자를 사용하는데, 이것은 해당 키와 오름, 내림 차순 지정 기호로 정렬기능을 수행한다.
~~~js
Template.postsList.helpers({
posts: function() {
return Posts.find({}, {sort: {submitted: -1}});
}
});
~~~
<%= caption "client/templates/posts/posts_list.js" %>
<%= highlight "3" %>
<%= commit "7-8", "등록일시로 post목록을 정렬한다." %>
약간의 작업이 들었지만, 마침내 우리는 앱에 사용자들이 콘텐츠를 안전하게 등록시키는 사용자 인터페이스를 구현했다!
하지만, 사용자가 콘텐츠를 등록하게 하는 어떤 앱이든 편집하고 삭제하는 기능도 구현해주어야 한다. 이것은 다음 장에서 다룰 것이다.