【node.js + express + mongodb】記事管理アプリを作ってみた

## 概要
記事を投稿、管理できるアプリを作成した。機能としては、アカウント登録、ログイン機能、記事作成＆編集＆削除機能がある。見た目は以下のようになる。

### 見た目
見た目は以下のようになる。

**ログイン画面**：bootstrap使用。
![1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2353023/b9c3e295-099e-0681-5ff8-b7998af763fc.png)

**登録画面**
![2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2353023/65994150-87dd-3b55-7fa4-4a7d79331265.png)

**記事リスト**：ページ分け、編集削除
![3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2353023/77f50a93-daef-99d8-f31a-6d7671f4e5a7.png)

**記事詳細**：書式がそのまま保存
![4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2353023/4e216de0-5ab6-8573-9a81-351e210dbfff.png)

**記事投稿**：リーチテキストエディタ使用
![5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2353023/669cb155-c87a-1111-4c0f-db7ab1364d12.png)



### ソースコード
ソースコードをgithubにアップした。
https://github.com/akikisai/Article-management-app

### 使用したライブラリー
使用したものやバージョンは以下になる。

"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"express-session": "^1.17.2",
"http-errors": "~1.6.3",
"moment": "^2.29.1",
"mongodb": "^4.4.0",
"morgan": "~1.9.1"


## 環境構築
まず環境構築を行う。
### node.js環境構築

以下のリンクからnode.js推奨版をダウンロードし、特に何も変更せずインストーラーの流れに沿ってインストールする。これでnpmコマンドが使えるようになる。
[node.jsダウンロードサイト](https://nodejs.org/ja/)

### mongoDB環境構築

以下のリンクの内容に沿って環境構築する。
[mongoDB環境構築](https://laboratory.kazuuu.net/installing-mongodb-windows-10/)

`mongo`コマンドを打ってmongo shellが現れれば成功。


## プロジェクト作成
デスクトップ上にプロジェクトを作成したい。まず`cd`コマンドで移動し、アプリケーションを素早く作成できるようにするためexpressをインストールする。
```
$ cd Desktop
$ npm install express-generator -g
```

textappというプロジェクトを作成する。この時デスクトップにtextappフォルダーが作られる。
その後textappフォルダーな中に入り、`npm install`で初期のパッケージを使用できるようにインストールする。
```
$ express -e textapp
$ cd textapp
$ npm install
```

※テキストエディタを使うが、ない場合visual studio codeもしくはatomが推奨。
[visual studio code公式ページ](https://code.visualstudio.com/)
[Atom公式ページ](https://atom.io/)

Node.jsでは、変更を有効にするためにプロセスを再起動する必要があるが、`nodemon`を使用してプロセスを自動的に再起動するので、余分な手順を排除る。これをインストールする。
```
$ npm install nodemon -g
```

`package.json`ファイル内の"start": "node ./bin/www"を以下のように変更し、これでnodemonに対応できる。
```
"start": "nodemon ./bin/www"
```

`npm start`でプロジェクト起動する。

```
$ npm start
```
ファイル内にポートが3000と設定されていることがわかり、
binファイル > wwwファイル > var port = normalizePort(process.env.PORT || '3000');
ウェブサイトに以下のリンクを入力すると、
http://localhost:3000/
welcome to expressが表示される。
ここで注意したいのが、次の作業に当たり、`npm start`を入力したターミナルは閉じないこと。

## mongoDBと接続
mongoDBの環境構築は済んでいて、ここでは新しいgit bashを開き、以下のコマンドでデータベース保存場所を決める。この場合C:\mongodb\dataに保存している。
```
$ mongod --dbpath "C:\mongodb\data"
```

mongoDBと接続するためには、`npm start`を入力したシェルと、`mongo`コマンドを打って出てきたmongoシェルが同時に存在しなければならない。`mongo`コマンドを打って出てきたmongoシェルがあるということはmongoDBに接続成功。なお、ターミナルにおいてmongoシェルにうまく入力できない場合、windowsのコマンドプロンプトでも大丈夫。

```
$ mongo
```
mongoDBとつなぎ、shell を使えるようになる。

データベースの中身を確認
```
>show dbs 
```

デフォルトはお以下のようになる。
```
admin   0.000GB
config  0.000GB
local   0.000GB
```

textappというデータベース作成
```use textapp```

userコレクションを作る。
```db.createCollection('users');```

コレクションを表示する。
```show collections```
正しく表示される。

[mongoDBコマンド参考用](https://qiita.com/k-staging/items/a386d272abb2c9b92f1a)


nodejsとmongodbを接続するために、mongodb nodejs driverを使う。
※ほかにもMongooseを使用する方法がある、こっちのほうが人気らしい。

インストール含め使い方は[mongodb nodejs driverドキュメント](https://mongodb.github.io/node-mongodb-native/)参照。

`cd textapp`でtextappフォルダ内でmongodb nodejs driverをインストールする。
```npm install mongodb --save```


ドキュメントの例を参考にし、mongodbに接続するコードを書く。ファイルの新規作成などの細かい手順は全部省略する（githubソースコードを見ればわかるので）この記事では主に機能についてまとめたい。

```javascript:model/index.js
var MongoClient = require('mongodb').MongoClient;

var url = 'mongodb://localhost:27017/textapp';
var dbName = 'textapp';

//データベースへ接続用関数
function connect(callback) {//関数化
  MongoClient.connect(url, function(err, client) {//コールバック関数

	//エラーの場合
    if (err) {
      console.log('データベース接続エラー', err);
	//エラーじゃない場合
    } else {
	//接続成功
      var db = client.db(dbName);
      callback && callback(db);
    }
  });
}


module.exports = {
  connect
}

```
接続する機能を関数化し、あとで使えるようにする。

## 登録＆ログイン

ルーティング設定をする。

```javascript:routes/index.js
// 登録ページ
router.get('/regist', function(req, res, next) {
  res.render('regist', {})
});

// ログインページ
router.get('/login', function(req, res, next) {
  res.render('login', {})
});
```

登録ページのコードは以下になる。

```html:views/login.ejs
<body>

  <div class="container">
	   <div class="login-container">
            <div class="form-box">
                <form action="/users/regist" method="post">
                  <input type="text" name="username" value="" placeholder="ユーザ名">
                  <input type="password" name="password" value="" placeholder="パスワード">
                    <button class="btn btn-info btn-block login" type="submit">登録する</button>
                </form>
                <div>アカウントをお持ちの場合、<br><a href="/login">ログイン</a>してください。</div>
            </div>
      </div>
  </div>

</body>
```

これにより、ユーザ名とパスワードは`"/users/regist"`へ送信される。

```javascript:routes/users.js
// 登録
router.post('/regist', function(req, res, next) {
  var data = {
    username: req.body.username,
    password: req.body.password
  };


  // データの接続
  model.connect(function(db) {
    //コレクションusersにデータ挿入する
    db.collection('users').insertOne(data, function(err, ret) {
      //登録失敗
      if (err) {
        console.log('登録失敗');
        res.redirect('/regist');
      } else {
        //登録成功、ログインページへ
        res.redirect('/login');
      }
    });
  })
})
```

送信されてきた情報はdataに格納され、データベースに接続し、照らし合わせることで登録成功か失敗か。ちなみにmongoDBインストール時、MongoDB compassをインストールしていれば、それを開いて、connectを押すことでmongoDBへ接続でき、GUI画面で登録された情報は実際あるのかを確認できる。[mongoDB compass参考](https://kennejs.com/entry/mongodb-compass-howtouse)


ログインページのコードは以下のようになる。
```javascript:views/login.ejs
<body>

  <div class="container">
	<div class="login-container">

            <div class="form-box">
                <form action="/users/login" method="post">
                    <input name="username" type="text" placeholder="ユーザ名">
                    <input name="password" type="password" placeholder="パスワード">
                    <button class="btn btn-info btn-block login" type="submit">ログイン</button>
                </form>
                <div>アカウントをお持ちでない場合、<a href="/regist">作成</a>できます。</div>

            </div>
        </div>

</div>

</body>
```

これにより、ユーザ名とパスワードは`"/users/login"`へ送信される。

```javascript:routes/users.js
// ログイン
router.post('/login', function(req, res, next) {
  var data = {
    username: req.body.username,
    password: req.body.password
  }

  //データベースに接続する。
  model.connect(function(db) {
    //コレクションに同じデータがあるか、あればログイン成功。
    db.collection('users').find(data).toArray(function(err, docs) {
      if (err) {
		//ログイン失敗→もう一回ログインページへ
        res.redirect('/login');
      }else {
	  //調べたdocsの結果が0以上であれば成功
        if (docs.length > 0) {
          // ログイン成功　session保存
          req.session.username = data.username;//ユーザーネームをセッションに保存
          res.redirect('/');
        } else {
          res.redirect('/login');
        }
      }
    })
  })
})
```

送信されてきた情報はdataに格納され、データベースに接続し、照らし合わせることでログイン成功か失敗か。失敗したらもう一度ログインページへ行く。また、上のコードでsessionを使用している。これはログイン状態を保存するため（毎回ログインするのはめんどくさいので）

sessionを使うためまずexpress-sessionをインストールする。textappフォルダ内で以下のコマンドを実行する。使い方は[express-sessionドキュメント](https://www.npmjs.com/package/express-session)参照。
```
$ npm install express-session -S
```

最初に設定をする。
```javascript:app.js
var session = require('express-session');

//セッション
app.use(session({
  secret: 'sai textapp',//何でもよい
  resave: false,
  saveUninitialized: true,
  cookie: { maxAge: 1000 * 60 * 20 } //cookie有効時間=ログイン有効期間
}))
```

ここでのログイン有効期間を20分とした。また上のログインが成功した場合ユーザ名をsessionに格納される。もしsessionにユーザ名がある場合、ログインせずにホームぺーズへ飛ぶことができるようになる。ユーザ名がニア場合、かつログインページと登録ページ以外ならば、ログインページへ戻る。これでログインしていない状態だとホームページは見れない。

```javascript:app.js
// sessionの中身判定
app.get('*', function(req, res, next) {
  var username = req.session.username;
  //パスを判断したいので宣言
  var path = req.path;
  
  //ログインページと登録ページ以外で、ユーザ名なければログインページへ飛ぶ。
  if (path != '/login' && path != '/regist') {
    if (!username) {
      res.redirect('/login');
    }
  }
  next();
})
```

## ログアウト
ヘッダーに投稿ボタンとログアウトボタンを配置する。
```html:views/bar.ejs
<li class="header_navi_item"><a href="/users/logout"><span>ログアウト</span></a></li>
```

ログアウトするには簡単で、sessionの中身を空っぽにしてlogin画面へ飛べばよい。
```javascript:routes/users.js
//ログアウト
router.get('/logout',function(req,res,next){
  req.session.username = null;
  res.redirect('/login');
})
```

## 記事投稿
ホームページに投稿するボタンを用意する。投稿画面`/write`へ飛ぶ。
```html:views/index.ejs
<li class="header_navi_item"><a href="/write"><span>投稿する</span></a></li>
```

write画面を作っていく。

```html:views/write.ejs
<body>

  <%- include('bar', {username: username}) %>
  <div class="article">
    <form action="/article/add" method="post">

      <input type="text" name="title" placeholder="タイトル" value="">
      <textarea name="content" class="xheditor" id="mytextarea"></textarea>

	  <input type="submit" value="投稿">
      
    </form>
  </div>

</body>
```

なお、ここでのbarはbar.ejsで、ヘッダーを作ります。ヘッダーは数画面同じものを使うので便利さを求めて別々にbar.ejsとして作り、includeによって呼び出して使う。この場合usernameを求めているので、routes/index.ejsの
router.get('/write', function(req, res, next) {})の中身にはusernameを取得しusernameを返す必要がある。そうするとusernameがbar(ヘッダー)のユーザ名のところに格納し、一番上の見た目のようにユーザ名が表示される。

ここで、投稿するためにリッチテキストエディターを実装したい。
リッチテキストエディタとは、HTMLを知らなくても文字、画像、表などをツールボタンを使って作成・編集することができるもの。実際にコピーしたものを投稿内容欄に張り付けても書式が残る。
→ブログ投稿をする際のエディタのようなもの

今回使用するのはTinyMCEというもの。[公式ページの始め方参照はこちら](https://www.tiny.cloud/docs/quick-start/)
やや古いですがとても分かりやすい[TinyMCE使い方記事はこちら](https://oxynotes.com/?p=11177)
なお、ほかのリーチテキストエディタを使いたい場合、[リッチテキストエディタ一覧](https://engineer.blog.lancers.jp/tools/wysiwyg_editor_best_7/)を参考して、各自調べてください。

ドキュメント通りにコードを書く。バージョン古くなるかもしれないので必ずドキュメント参照推奨。ドキュメントのやり方だと、
```
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/5/tinymce.min.js" referrerpolicy="origin"></script>
```
`http://`から始まって直接読み込むことができるので便利ですが、今回はTinyMCEを直接[ダウンロード](https://www.tiny.cloud/get-tiny/)ページから`Downlord TinyMCE SDK now`をクリックし、ダウンロードして解凍して中身のファイルをtextapp/public/javascriptsの中にいれ、リンクは代わりにtinymce.min.jsを適切なパスで読み込む。今回の例では
javascripts/tinymce_5.10.3/tinymce/js/tinymce/
の中に存在する。

write.jsのheadタグ内をいじる。2つのscriptタグを追加した。
```html:views/write.ejs
<head>

  <title>記事作成</title>
  <%- include head %>

  <script src="javascripts/tinymce_5.10.3/tinymce/js/tinymce/tinymce.min.js" referrerpolicy="origin"></script>

   <script>
     tinymce.init({
       selector: 'textarea',
       language: 'ja'
     });
   </script>

</head>
```

言語を日本語化にしたいので、[TinyMCE言語パッケージ](https://www.tiny.cloud/get-tiny/language-packages/)ページからjaを見つけ、ダウンロードする。解凍したlangsフォルダの「ja.js」を「tinymce/」にある「langsフォルダ」へ移動する。
うえのようにlanguage: 'ja'と記入すれば完成。

記事投稿するので、データベースに新しいコレクションを追加。
mongo shellで以下のコマンドを入力し、articlesコレクションを作る。

```
use textapp
db.createCollection('articles');
```

次は実際に記事書いたら、記事を投稿する機能を持つボタンを作る。ここで入力内容はformの`/article/add`へ行く。ということで作成していく。

```javascript:routes/article.js

// 追加
router.post('/add', function(req, res, next) {
	//データを受け取る
	var data = {
      title: req.body.title,//タイトル
      content: req.body.content,//記事内容
      username: req.session.username,//ユーザ名
      id: Date.now()//時間

	//データベースに接続し、データを挿入する
    model.connect(function(db) {
      db.collection('articles').insertOne(data, function(err, ret) {
        if(err) {
          console.log('文件发布失败', err)
		//失敗、もう一回記事投稿ページ
          res.redirect('/write')
        } else {
		//成功、ホームページへ
          res.redirect('/')
        }
      })
    })
  }
})

```

正しく入っているかどうか、Mongo shellで確認するか、MongoDB compassで確認する。

## 記事表示（ホームページ）

まずroutes/index.jsの`"/"`へ飛ぶ時の処理を編集する。
時間を扱いたいので、gitbashでtextapp内においてmomentをインストール。
```npm install moment -S```
[公式ドキュメントはこちら](https://momentjs.com/docs/#/use-it/node-js/)


```javascript:routes/index.js
/* ホームページ*/
router.get('/', function(req, res, next) {
  var username = req.session.username;

  //データベース接続
  model.connect(function(db) {
    // すべての記事を調べる。
    db.collection('articles').find().toArray(function(err, docs) {
      var list = docs;
	//ここでマップを使って、時間を表しているidを日付形式に変換し、timeと名付ける。
    　list.map(function(ele, index) {
        ele['time'] = moment(ele.id).format('YYYY-MM-DD HH:mm:ss')
     res.render('index', { username: username, data: list });    
    })
  })

});

```


次に記事表示画面を作る。取得されたitemの中に必要な情報が入っている。
```html:views/index.ejs
<% list.map(function(item,index){ %>
        <div class="row">
          <span><%= index+1 %></span>
          <span><%= item.username %></span>
          <span><%= item.title %></span>
			<!-- 定義したtimeを使用している。-->
          <span><%= item.time %></span>
          <span>
            <a href="">編集</a>
            <a href="">削除</a>
          </span>
        </div>
      <% }) %>

```

## 記事リストページ分け
記事が多い場合、ページ分けして1ページに何個記事表示できるかを設定。
routes/index.jsやviews/index.jsを編集する。

```javascript:routes/index.js
/* ホームページ*/
router.get('/', function(req, res, next) {
  var username = req.session.username;
  //フロント側で今は何ページ目にいるかデータを受け取る、なければ1
  var page = req.query.page || 1;
  var data = {
    total: 0,//トータル何ページ必要か
    curPage: page,//現在あるページ数
    list:[]//今表示しているページの記事リスト
  }
  var pageSize = 2;//表示しているページに記事何個ひょうじするか

  model.connect(function(db) {
    // すべての記事を調べる。
    db.collection('articles').find().toArray(function(err, docs) {
      //全部の記事数÷1ぺ時に表示する記事数＝トータル何ページ必要か
      //Math.ceil() 関数は、引数として与えた数以上の最小の整数を返す
      data.total = Math.ceil(docs.length / pageSize)
      // 今のページの記事リストを調べる。
      model.connect(function(db) {
        // sort()  limit()  skip()
        //降順で、一回に調べる記事数を限定（2）、スキップする個数（1ページ目なら(1-1)*2で0,スキップせず最初から2つ調べる）
        db.collection('articles').find().sort({_id: -1}).limit(pageSize).skip((page-1)*pageSize).toArray(function(err, docs2) {
          //時間変換
	      docs2.map(function(ele, index) {
            ele['time'] = moment(ele.id).format('YYYY-MM-DD HH:mm:ss')
          })
          data.list = docs2
          res.render('index', { username: username, data: data });
        })
      })
    })
  })

});

```
記事を分ける際、必要な情報は、
1. total:  トータルで何ページ必要か
2. cuiPage:  現在あるページ数 (ここでは必要ないが後で削除機能のために使う)
3. list:  今表示しているページの記事のリスト
4. page:  フロント側で今は何ページ目にいるかデータ
5. pageSize:  1ページに表示したい記事数

であり、取得した記事数によって、
total ＝記事数÷pageSize（小数の場合大き目の整数に変換）

```html:views/index.js
<body>
    <%- include('bar', {username}) %>

    <div class="list">
      <!-- ページ分け、何ページ目 -->
      <div class="pages">
        <% for(let i=1; i<=data.total; i++) { %>
		<!-- ここでクリックすると今いる何ページ目かが"/"にわたり、そのページ内容を表示したい-->
          <a class="pages_style" href="/?page=<%= i %>"><%= i %></a>
        <% } %>
      </div>

      <div class="row">
        <span>順番</span>
        <span>作者</span>
        <span>タイトル</span>
        <span>公開日</span>
        <span>
          <i class="fa-solid fa-pen-to-square"></i>
        </span>

      </div>

      <!-- 記事リスト -->
      <% data.list.map(function(item,index){ %>
        <div class="row">
          <span><%= index+1 %></span>
          <span><%= item.username %></span>
          <span><%= item.title %></span>
          <span><%= item.time %></span>
          <span>
            <a href="">編集</a>
            <a href="">削除</a>
          </span>
        </div>
      <% }) %>

    </div>

</body>
```
for文でdata.totalを使用し、実際のページ数を表示。また、ページボタンに今のいるページ何番目かを意味するpageを`"/"`に渡しているので、この情報を使って表示すべき記事を選出し、data.listに格納される。

## 記事削除
views/index.jsの削除のところを下のように直す。
削除するために必要な情報は、記事のidはもちろん、もう1つは今何ページ目にいるかという、pageが必要。なお、?id=<%=item.id%>&page=<%=data.curPage%>のところ、
?の後ろにはURLパラメータが来ていて、この値を行先に渡すイメージ。
```html:views/index.js
<a href="/article/delete?id=<%=item.id%>&page=<%=data.curPage%>">削除</a>
```

routes/article.jsも編集。
 ```javascript:routes/article.js

// 記事削除
router.get('/delete', function(req, res, next) {
  var id = parseInt(req.query.id)
  var page = req.query.page
  //接続
  model.connect(function(db) {
	//idで調べる
    db.collection('articles').deleteOne({id: id}, function(err, ret) {
      if (err) {
        console.log('削除失敗');
      } else {
        console.log('削除成功');
      }
	  //リダイレクトする
      res.redirect('/?page='+page);
    })
  })
})
 
 ```

 なおこの状態では、たとえ削除したページに記事が1つもなくても、表示できるので、不適切である。これを直すため、routes/index.jsに判定条件を追加する。

```javascript:routes/index.js
/* ホームページ*/
router.get('/', function(req, res, next) {
  var username = req.session.username;
  //フロント側で今は何ページ目にいるかデータを受け取る、なければ1
  var page = req.query.page || 1;
  var data = {
    total: 0,//トータル何ページ必要か
    curPage: page,//現在あるページ数
    list:[]//今表示しているページの記事リスト
  }
  var pageSize = 2;//表示しているページに記事何個ひょうじするか

  model.connect(function(db) {
    // すべての記事を調べる。
    db.collection('articles').find().toArray(function(err, docs) {
      //全部の記事数÷1ぺ時に表示する記事数＝トータル何ページ必要か
      //Math.ceil() 関数は、引数として与えた数以上の最小の整数を返す
      data.total = Math.ceil(docs.length / pageSize)
      // 今のページの記事リストを調べる。
      model.connect(function(db) {
        // sort()  limit()  skip()
        //降順で、一回に調べる記事数を限定（2）、スキップする個数（1ページ目なら(1-1)*2で0,スキップせず最初から2つ調べる）
        db.collection('articles').find().sort({_id: -1}).limit(pageSize).skip((page-1)*pageSize).toArray(function(err, docs2) {
			//記事がなければ
           if (docs2.length == 0) {
             //前のページに戻る、最小1
             res.redirect('/?page='+((page-1) || 1))
           } else {
		   //記事があれば、時間の処理をする。
            docs2.map(function(ele, index) {
              ele['time'] = moment(ele.id).format('YYYY-MM-DD HH:mm:ss')
            })
            data.list = docs2
           }
          res.render('index', { username: username, data: data });
        })
      })
    })
  })

});

```
 
 ## 記事編集

 writeへ飛ぶ。同じくidとcurPageが必要。
 ```html:views/index.js
<a href="/write?id=<%=item.id%>&page=<%=data.curPage%>">編集</a>
```

router.get('/write', function(req, res, next) {})は以下のようになる。
```javascript:routes/index.js
// 投稿＆編集ページ
router.get('/write', function(req, res, next) {
  var username = req.session.username || ''
  var id = parseInt(req.query.id)
  var page = req.query.page
  var item = {
    title: '',
    content: ''
  }
  if (id) {  // idがあれば編集
    model.connect(function(db) {
      db.collection('articles').findOne({id: id} , function(err, docs) {
        if (err) {
          console.log('失败')
        } else {
          item = docs
          item['page'] = page
          res.render('write', {username: username, item: item})
        }
      })
    })
  } else {  // idがなければ追加
    res.render('write', {username: username, item: item})
  }
})
```

また、write画面も編集する。
```html:views/write.ejs
<body>

  <%- include('bar', {username: username}) %>
  <div class="article">
    <form action="/article/add" method="post">

      <input type="hidden" name="id" value="<%- item.id %>">
      <input type="hidden" name="page" value="<%- item.page %>">
      <input type="text" name="title" placeholder="タイトル" value="<%= item.title %>">
      <textarea name="content" class="xheditor" id="mytextarea"><%- item.content %></textarea>
      <% if (item.id) { %>
        <input type="submit" value="編集">
      <%} else {%>
        <input type="submit" value="投稿">
      <%}%>
    </form>
  </div>

</body>
```
ボタンもif文でidがあれば編集、なければ投稿と表示。
また、編集をするためにpageとidを渡さなければならない。
編集成功したらpageに飛びたいので。
```
<input type="hidden" name="id" value="<%- item.id %>">
<input type="hidden" name="page" value="<%- item.page %>">
```
hidenを使って見えなくし、配置する。
`/article/add`のところも編集する。同じくidで編集する。

```javascript:routes/article.js

// 追加、編集
router.post('/add', function(req, res, next) {
  var id = parseInt(req.body.id)
  if (id) {  //編集
    var page = req.body.page
    var title = req.body.title
    var content = req.body.content
    model.connect(function(db) {
      db.collection('articles').updateOne({id: id}, {$set: {
        title: title,
        content: content
      }}, function(err, ret) {
        if (err) {
          console.log('編集失敗', err)
        } else {
          console.log('編集成功')
          res.redirect('/?page='+page)
        }
      })
    })
  } else {   //追加
    var data = {
      title: req.body.title,
      content: req.body.content,
      username: req.session.username,
      id: Date.now()
    }
    model.connect(function(db) {
      db.collection('articles').insertOne(data, function(err, ret) {
        if(err) {
          console.log('失败', err)
          res.redirect('/write')
        } else {
          res.redirect('/')
        }
      })
    })
  }
})
```


## 記事表示

タイトルのところに行先を設定。
```html:view/index.ejs
<span><a href="/detail?id=<%=item.id%>"><%= item.title %></a></span>
```

記事表示ページを作成する。
```html:view/detail.ejs
<!DOCTYPE html>
<html lang="ja">
<head>

  <title><%=item.title%></title>
  <%- include head %>
</head>
<body>

  <%- include('bar', {username: username}) %>
  <div class="detail">
    <div class="title"><%=item.title%></div>
    <div class='desc'>
      <span>作者：<%=item.username%></span>
      <span>投稿日：<%=item.time%></span>
    </div>
    <div class="content"><%-item.content%></div>
  </div>

</body>
</html>

```

routes/index.jsにもコードを追加する。
```javascript:routes/index.js
// 記事詳細ページ
router.get('/detail', function(req, res, next) {
  var id = parseInt(req.query.id)
  var username = req.session.username || ''
  model.connect(function(db) {
    db.collection('articles').findOne({id: id}, function(err, docs) {
      if (err) {
        console.log('失败', err)
      } else {
        var item = docs
        item['time'] = moment(item.id).format('YYYY-MM-DD HH:mm:ss')
        res.render('detail', {item: item, username: username})
      }
    })
  })
})

```

これでタイトルを押したら、該当記事の詳細を確認できる。

## その他
このままだと、実はほかの人でも他人の記事を編集＆削除できる。これはまずいので、変えていく。やり方は簡単、以下のように修正すればいい。
```html:views/index.ejs
<!-- 記事リスト -->
      <% data.list.map(function(item,index){ %>
        <div class="row">
          <span><%= index+1 %></span>
          <span><%= item.username %></span>
          <span><a href="/detail?id=<%=item.id%>"><%= item.title %></a></span>
          <span><%= item.time %></span>
          <span>
            <% if (item.username===username) { %>
              <a href="/write?id=<%=item.id%>&page=<%=data.curPage%>">編集</a>
              <a href="/article/delete?id=<%=item.id%>&page=<%=data.curPage%>">削除</a>
            <%} else {%>
              <span class="grey">編集</span>
              <span class="grey">編集</span>
            <%}%>
          </span>
        </div>
      <% }) %>
```

ここではif文を使って判定している。usernameはsessionから取得した今ログイン中のユーザ名、item.usernameとはそれぞれの記事の作者名である。一致しなければ編集&削除ができない。

## デプロイ（公開）
まだ未公開ですが、EC2で公開したいと思っていて、以下の記事参考になりそうなのでいったんメモしておきます。

**ec2**
[【AWS】EC2とは？概要から使い方までざっくり解説（実践あり）](https://zenn.dev/tomoshimizu/articles/eedb6856df9c90)

**node.js**
[[AWS] 手順に沿った画像付きでAmazon EC2にNode.js, Express環境の構築する](https://dev.classmethod.jp/articles/building-node-js-express-dev-env-in-aws-ec2-with-images/)
[チュートリアル: Amazon EC2 インスタンスでの Node.js のセットアップ](https://docs.aws.amazon.com/ja_jp/sdk-for-javascript/v2/developer-guide/setting-up-node-on-ec2-instance.html)
[AWS EC2でNodeを動作させる](https://qiita.com/oishihiroaki/items/bc663eb1282d87c46e97)

**mongodb**
[MongoDB CommunityEditionをAmazonLinuxにインストールします](https://docs.mongodb.com/manual/tutorial/install-mongodb-on-amazon/)

***
途中までやっていた。コマンドメモ↓↓
```
$ ssh -i textapp-key.pem ec2-user@18.183.176.255

$ ssh -i textapp-key.pem ec2-user@18.183.176.255
$ sudo yum update
$ sudo yum install git gcc-c++ make openssl-devel
$ nvm install v16.14.0
$ nvm alias default v16.14.0
$ npm install express-generator -g

npm WARN deprecated mkdirp@0.5.1: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)

$ npm update -g mkdirp
$ npm install -g npm@8.5.3

$ git clone https://github.com/akikisai/Article-management-app.git
$ ls -a
$ cd Article-management-app/
$ npm install


$ grep ^NAME /etc/*release
/etc/os-release:NAME="Amazon Linux"

$ cd /etc/yum.repos.d
$ sudo touch mongo-org-5.0.repo
$ sudo su
# vi mongo-org-5.0.repo


[mongodb-org-5.0]
name=MongoDB Repository
baseurl=https://repo.mongodb.org/yum/amazon/2/mongodb-org/5.0/x86_64/
gpgcheck=1
enabled=1
gpgkey=https://www.mongodb.org/static/pgp/server-5.0.asc

:wq!

# sudo yum install -y mongodb-org

exclude=mongodb-org,mongodb-org-database,mongodb-org-server,mongodb-org-shell,mongodb-org-mongos,mongodb-org-tools

# ps --no-headers -o comm 1
systemd

# sudo systemctl start mongod

# sudo systemctl status mongod
 Active: active (running) since Sat 2022-03-05 12:30:03 UTC; 29s ago

$ mongosh
>use textapp

>db.createCollection('users');
>db.createCollection('articles');


$ cd Article-management-app/
node ./bin/www

```

躓いた。。もう少しいろいろ試したい。

***

## 問題点
今回は機能を作ってみるだけにしましたが、まだしっかりとしたエラー処理を書いていなく(ユーザ名判定など) マークダウンにも対応させたいと思っていて、全然新しい機能を追加する余地はあるが、今回は一旦締め切りにする。