todoList管理アプリを作ってみる②

nginx - sinatra - mysql

今回作成する Webサーバ の構成は,
 サーバ
  OS : ubuntu 22.04 LTS
  Webサーバ : nginx
  APサーバ : ruby ( sinatra )
  DB : mysql
  アプリ : todoList 管理アプリ
 開発環境
  mac

 です。少しずつ機能を追加していく予定です。

 さて、第1回で疎通した ubuntu – nginx – ruby ( sinatra ) – mysql の webアプリサーバ で todoList を作ってみます。

 開発機は mac で、ソース管理は visual studio code ( 以下 vscode ) と git を使います。

1 開発環境 ( vscode , git ) を準備します。
 vscode は公式ページからダウンロードしてください。mac は git がプレインストールされています。それを使いましょう。

( input )
@dev:~$ git -v

( output )
git version 2.39.3 (Apple Git-145)

2.作業フォルダをつくります。
 開発機で作業ディレクトリを作成し、その場所を vscode で開きます。vscode にと git ( github ) をリンクしておけば、自動で git を使ってバージョン管理をしてくれます。

 今回作成するアプリは以下のような表示の todoList です。

 構成は
  index.html , styles.css , main.js : /var/www/html/ に配置
  app.rb : ~/ruby-work/ に配置
  @server(ssh):~/ruby-work$ ruby app.rb で ルビーを実行。
  開発機のブラウザで http://192.168.0.11 ( うちの場合 ) で表示します。

 まずは index.html と css を入力します。大きく分けて、①入力エリア、②未達成エリア、③達成済エリアに分けています。データベースから取得した値を動的に生成するために必要な箇所に id を付与するなどして備えます。

<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <link rel="stylesheet" href="styles.css">
        <title>todoList app</title>
    </head>

    <body>
        <h1>todoList ver1.0</h1>
        <div id="input-area">
            <input id="todo-input" placeholder="todoを入力" type="text" />
            <button id="add-button">追加</button>
        </div>
        
        <h2>未達成 todo 一覧</h2>
        <table id="incomplete-area" border="1">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>todo</th>
                    <th>作成日時</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody id="mi-tbl-body">
                <tr>
                    <td></td>
                    <td>error</td>
                    <td></td>
                    <td><button id="comp-btn">達成</button><button id="dlt-btn">削除</button></td>
                </tr>
            </tbody>
        </table>

        <h2>達成済 todo 一覧</h2>
        <table id="complete-area" border="1">
            <thead>
                <tr>
                    <th>ID</th>
                    <th>todo</th>
                    <th>作成日時</th>
                    <th>操作</th>
                </tr>
            </thead>
            <tbody id="zumi-table-body">
                <tr>
                    <td></td>
                    <td>ERROR</td>
                    <td></td>
                    <td><button id="comp-btn">達成</button><button id="dlt-btn">削除</button></td>
                </tr>
            </tbody>
        </table>

        <script src="main.js"></script>
    </body>
</html>
body {
    font-family: sans-serif;
}
  
input {
  border-radius: 16px;
  border: none;
  padding: 6px 16px;
  outline: none;
}

button {
  border-radius: 16px;
  border: none;
  padding: 4px 16px;
}

button:hover {
  background-color: #ff7fff;
  color: #fff;
  cursor: pointer;
}

li {
  margin-right: 8px;
}

#input-area {
  background-color: #c1ffff;
  width: 600px;
  height: 30px;
  border-radius: 8px;
  padding: 8px;
  margin: 8px;
}

#incomplete-area {
  background-color: #c6ffe2;
  width: 600px;
  min-height: 200px;
  padding: 8px;
  margin: 8px;
  border-radius: 8px;
}

#complete-area {
  background-color: #ffffe0;
  width: 600px;
  min-height: 200px;
  padding: 8px;
  margin: 8px;
  border-radius: 8px;
}

.title {
  text-align: center;
  margin-top: 0;
  font-weight: bold;
  color: #666;
}

 次に javaScript を記述します。javaScript がコールされた時にまず main() を実行します。あとは html でイベントごとに関連付けした関数がコールされた時の動作を記述していきます。ruby とのやりとりは fetch( ‘url’ ) で行っています。この url は nginx のリバースプロキシの設定で sinatra が受け取るように設定してあります。

const miTableBody	= document.getElementById( 'mi-tbl-body' )
const zumiTableBody	= document.getElementById( 'zumi-table-body' )
const listTodoInput = document.getElementById( 'todo-input' )
const listAddButton = document.getElementById( 'add-button' )

async function loadTasks() {
	const response = await fetch( '/api/list' )
	const responseBody = await response.json()

	const tasks = responseBody.tasks

	while( miTableBody.firstChild ) {
		miTableBody.removeChild( miTableBody.firstChild )
	}

	while( zumiTableBody.firstChild ) {
		zumiTableBody.removeChild( zumiTableBody.firstChild )
	}

	tasks.forEach( ( tasks ) => {
		const trRowElement = document.createElement( 'tr' )

		const tdIdElement = document.createElement( 'td' )
		tdIdElement.innerText = tasks.id

		const tdContentElement = document.createElement( 'td' )
		tdContentElement.innerText = tasks.todo

		const tdCreatedAtTdElement = document.createElement( 'td' )
		tdCreatedAtTdElement.innerText = tasks.createdAt

		const td2Element = document.createElement( 'td' )
		const button1Element = document.createElement( 'button' )
		const button2Element = document.createElement( 'button' )

		if( tasks.todoState == 0 ) {
			button1Element.innerText = "完了"
			button1Element.className = "comptt-btn"
			button1Element.addEventListener( "click" , { id: tasks.id , handleEvent: completeTask } )
	
			button2Element.innerText = "削除"
			button2Element.id = tasks.id
			button2Element.addEventListener( "click" , { id: tasks.id , handleEvent: deleteTask } )
		}else{
			button1Element.innerText = "戻す"
			button1Element.className = "undo-btn"
			button1Element.addEventListener( "click" , { id: tasks.id , handleEvent: undoTask } )
	
			button2Element.innerText = "削除"
			button2Element.className = "dele-btn"
			button2Element.addEventListener( "click" , { id: tasks.id , handleEvent: deleteTask } )
		}

		td2Element.appendChild( button1Element )
		td2Element.appendChild( button2Element )

		trRowElement.appendChild( tdIdElement )
		trRowElement.appendChild( tdContentElement )
		trRowElement.appendChild( tdCreatedAtTdElement )
		trRowElement.appendChild( td2Element )

		if( tasks.todoState == 0 ) {
			miTableBody.appendChild( trRowElement )
		}else{
			zumiTableBody.appendChild( trRowElement )
		}
	})
}

async function registerTask() {
	const todo = listTodoInput.value

	const requestBody = {
		todo: todo ,
		todoState: 0
	}

	await fetch( '/api/list' , {
		method: 'POST' ,
		body: JSON.stringify( requestBody )
	})

	await loadTasks()
}

async function deleteTask( e ) {

	const requestBody = {
		id: this.id
	}

	await fetch( '/api/tasks_delete' , {
		method: 'POST' ,
		body: JSON.stringify( requestBody )
	})

	await loadTasks()
}

async function undoTask( e ) {

	const requestBody = {
		id: this.id
	}

	await fetch( '/api/tasks_undo' , {
		method: 'POST' ,
		body: JSON.stringify( requestBody )
	})

	await loadTasks()
}

async function completeTask( e ) {

	const requestBody = {
		id: this.id
	}

	await fetch( '/api/tasks_complete' , {
		method: 'POST' ,
		body: JSON.stringify( requestBody )
	})

	await loadTasks()
}

async function main() {
	listAddButton.addEventListener( 'click' , registerTask )
	
	await loadTasks()
}

main()

 最後に ruby を記述します。get または post で受け取った url ごとに処理内容を記述していきます。
一般的な crud ( create , read , update , delete ) に対応しています。

require 'sinatra'
require 'mysql2'

get '/api/hello' do
  {
    message: 'Hello sinatra'
  }.to_json
end

get '/api/list' do
  client = connect

  result_set = client.query( 'select id , todo , todoState , created_at from todoList' )
  tasks = result_set.map do |row|
    {
      id: row['id'] ,
      todo: row['todo'] ,
      todoState: row['todoState'] ,
      createdAt: row['created_at']
    }
  end

  client.close
  {
    tasks: tasks
  }.to_json
end

post '/api/list' do
  request_body = JSON.parse request.body.read

  todo = request_body['todo']

  client = connect

  statement = client.prepare( 'insert into todoList( todo , todoState ) values ( ? , 0 )' )
  statement.execute( todo )

  client.close
end

post '/api/tasks_complete' do
  request_body = JSON.parse request.body.read

  id = request_body['id']

  client = connect

  statement = client.prepare( 'update todoList set todoState = 1 , created_at = current_timestamp where id = ?' )
  statement.execute( id )

  client.close
end

post '/api/tasks_undo' do
  request_body = JSON.parse request.body.read

  id = request_body['id']

  client = connect

  statement = client.prepare( 'update todoList set todoState = 0 , created_at = current_timestamp where id = ?' )
  statement.execute( id )

  client.close
end

post '/api/tasks_delete' do
  request_body = JSON.parse request.body.read

  id = request_body['id']

  client = connect

  statement = client.prepare( 'delete from todoList where id = ?' )
  statement.execute( id )

  client.close
end

def connect
  Mysql2::Client.new(
    :host => "localhost",
    :database => "todoList",
    :username => "todoList",
    :password => 'password',
    :connect_timeout => 5
  )
end

 これらのファイルが開発環境で入力できたら、サーバに転送します。転送には scp コマンドを利用します。

@dev:~$ scp ruby-work/app.rb yosuke@192.168.0.11:~/ruby-work/
@dev:~$ scp html/index.html html/styles.css html/main.js yosuke@192.168.0.11:~/
@server(ssh):~$ sudo mv index.html styles.css main.js /var/www/html

 それぞれのファイルを所定の場所に配置して ruby app.rb を実行し、開発機のサーバから確認してください。

コメント

タイトルとURLをコピーしました