はじめに
こんにちは。初めてQiitaに記事を投稿してみます。Express.jsとVue.jsで家賃管理アプリを作ったため、その際の経験をまとめました。わかりにくい部分もたくさんあると思いますが、どうぞよろしくお願いします。
簡単に自己紹介します。社会人2年目でシステムを作っている会社でお仕事してます。学生時代には、映像処理の研究をしていました。PythonやC++でプログラムを書いてました。ただ、自分だけがプログラムを動かせれば良かったので、必要最低限のプログラミングスキルしかなく、綺麗なコードを書けるスキルはまだありません。社会人になるまでは、常時動くシステムは作ったことがなく、サーバって何?Webの仕組みってどうなっているの?データベースって何?SQL?何それ美味しいの?という状態でした。社会人になってから、システム周りの議論があると、前提知識が足りず、今までの経験での知識や書籍での勉強だけでは不足しているな、と感じる場面が増えて来ていました。
現在は、研究寄りのチームのため、また仕事で商用システムを実際に開発したことがありません。しかし、商用のシステムを支えるための研究をしているため、様々な部門の方と現状のシステムの問題などを議論する場が結構あります。そのため、Qiitaの記事や書籍などでシステム周りの勉強をして来ました。(ここに関しては、別の記事を記載しようと思います。)社会人一年目では、自分もよく使うWebのシステムの動きを理解してみようと思い、Node.jsやVue.jsなどの書籍を購入して、書籍通りにコーディングして動かし、ざっくりと動きを理解する方法で勉強してきました。そのおかげもあり、ざっくりとですが簡単なWebアプリを動かす全体像が見え始めました。そこで、自分で何かオリジナルのアプリを作り、システム開発の流れやフロントエンドやバックエンドの役割の理解などを深めてみようと思いました。
記事を投稿しようと思った理由は、今回のアプリ開発でもQiitaの記事によく助けられたため、自分の経験もQiitaに記載し、開発初心者の方などに知見を提供できれば良さそうと思ったためです。
以下では、ざっくりと下記の内容をまとめております。
- アプリ開発の背景
- 開発の目的
- 開発の流れ
- コーティング内容
- まとめ
詳しくは、Qiitaの目次をご覧ください。
アプリ開発の背景
実家での家賃管理
私は、まだ実家に住んでおります。社会人になったこともあり、親に家賃を支払っております。また、兄弟も一緒に住んでいるおり、その兄弟も親に家賃を支払っています。しかし、親はいつ誰が何月分の家賃を支払ったのかをメモしておらず、誰が何月分まで支払ったか忘れてしまうケースが多々ありました。
このような背景もあり、家賃管理アプリを作ることにしました。
(実際に家族に使ってもらう、という使命感を持つことで、途中でこのアプリ開発をやめないようにしようと考えていました。アプリストアで提供されているツールを使って管理する、紙に書いて管理する、など別の手段で家賃を管理する方がおそらくとても楽だろうな、、、と薄々思っていました。今回は、アプリ開発の経験を積むと言うことを優先してしまったので、"真に家族に役立つものなのか"の観点でみると、今回のような自作アプリが正解なのかは議論の余地があると思います。このシステムは本当に作るべきなのか、みたいな議論は実際のお仕事では非常に重要だと思ってます。)
自分のアプリ開発知識
今まで勉強してきた内容は、主にNode.jsのExpress.jsやRuby on Railsを使ってWebアプリを作るものでした。これらのツールでは、例えばExpress.jsではejsなどのWebページの表示もしてくれるツールがあり、Express.jsだけでちょっとしたWebアプリが作れていました。Express.jsやRuby on Railsだけで完結してしまうと、Web系の仕事などでよく使われているフロントエンドとバックエンドの違いがあまりわからない状態でした。
最近、フロントエンド側でVue.jsやReact.jsなどのツールが注目されていることを知り、初心者でもわかりやすそうなVue.jsを勉強し始めていました。このVue.jsとExpress.jsを組み合わせて、フロントエンドとバックエンドで役割を分けて開発してみると、どうなるのか試したくなりました。
上記の個人的な理由から、Vue.jsとExpres.jsをうまく組み合わせてアプリを動かす一つのシステムを開発してみることにしました。また、ただただコーディングするだけではなく、実際の開発の流れを擬似体験(一人での開発であるが...)したいと思い、要件の定義や設計の情報もなるべく細かく記録して開発を進めるするようにしました。
開発の目的
ざっくりと今回の開発目的をまとめたいと思います。
- 家賃管理アプリを作り、家族の家賃支払い状況を家族全員が確認可能である状態を作ること
- 自分のオリジナルアプリを作り、システム開発の流れを経験を通して理解すること
- 勉強ではっきりイメージできてなかったフロントエンドとバックエンドの役割の違いを理解すること
開発の流れ
ここからは、少しメモ程度になりますが、家賃管理アプリを実際に開発する際の流れを記載していきます。
(おかしいところがたくさんあると思いますので、有識者の方がいらっしゃいましたら、指摘などのコメントお待ちしております。)
要件定義
ユーザごとの要件
支払い者(子)
- 支払い状況を確認したい
- 支払いの状況を更新したい
- 支払いが遅れた理由を記載したい
- 支払い料金が変わっていることを確認したい
受け取り者(親)
- 支払い者の支払い状況を確認したい
- 受け取った後に、支払いのチェックをしたい
- 支払い金額を変更したい
- 支払い者ごとに支払い状況を確認したい
共通する要件
- スマホで確認したい
- 簡単なドメイン名でアプリを触りたい
機能要件
- 機能一覧
- 表示機能
- 更新機能
非機能要件
- 性能
- ユーザは家族数人で、ラズパイ程度でOK
- 信頼性
- データは記録しておきたい
- DBのデータが消えるのは困る
- 信頼性
- 外部には公開しない、LAN内からのみアクセス可能
- 個人情報は載せない、支払い状況のみを記載
- 使用性
- UIは、流行りのSPAを採用
- スマホからアクセスが可能なUIにする
- その他
- ハードはラズパイを使用
- ラズパイはLANに有線で接続し、IPアドレスは固定させる
- (結局やってない...)できれば、ローカルでのみ使えるドメイン名を作成する
全体のシステム構成
画面設計
表を書くのが面倒でしたので、画像で貼り付けました。
データフロー設計
memo
上記のページを参考にすると良さそう。
ログイン時とデータの更新処理時は、少し複雑な処理だと思ったので、ざっとフローチャートを作成して、どのような動きになりそうか確認する。
画面デザイン設計
操作端末
- スマホ(メインで)
- PC(自分の運用で)
作成画面
- フォーム画面
- ログイン
- 状況編集
- テーブル表示画面
- 支払い状況一覧
- データ表示画面
- 支払い状況詳細情報
- 金額に関する詳細情報
主にスマホで操作するので、スマホでのイメージ図をAdobe XDで作成した。
DB設計
データベースには以前使ったことがあるMySQLを活用した。
必要な情報
- ユーザ名
- 支払い金額
- 支払い年月
- 支払いの有無
- 支払い日
- 支払い金額に対するコメント
- ユーザの支払い最終更新日
- 親のチェックの有無
- 親のチェック日
- 親のチェックに対するコメント
- ...
テーブルの設計
DB名
- express_rent
テーブル
- ユーザテーブル
- ユーザID、ユーザ名、役割ID
- 役割テーブル
- 役割ID、役割名
- 支払いチェックテーブル
- 支払ID、ユーザID、支払年月、支払金額、支払状態ID、支払日、支払コメント、親のチェック状態ID、親のチェック日、親のコメント
- 支払い状態テーブル
- 支払状態ID、状態
- 親のチェック状態テーブル
- 親のチェック状態ID、状態
このDBの設計はあまりよくはなさそう。DBの設計方法も勉強しないと。
コーティング内容
ここから、少し長くなります。あとこのままのやり方で動くが不安です。アプリを作った3ヶ月後くらいにこの記事を記載しているので、もしかすると、何か情報が抜けていて動かないかもしれません。雰囲気だけ掴んでいただければと思います。
また、コードはかなり雑に書いてますので、読みにくいと思います。。。
フォルダの準備
- アプリ全体のフォルダ
- ~/rentApp
- サーバ側
- ~/rentApp/server/express_mysql/
- フロント側
- ~/rentApp/frontend/vue
cd ~
mkdir ~/rentApp
mkdir ~/rentApp/server/
mkdir ~/rentApp/frontend/
DBの準備
MySQLの準備
// MySQLがそのままapt-getできないみたいなのでmariadbで対応する。
sudo apt-get install mariadb-client mariadb-server
Rootユーザでの処理
-- DBの作成
create database express_rent;
use express_rent;
-- ユーザの作成、権限の付与
CREATE USER 'express1'@'localhost' IDENTIFIED BY 'xxxxx';
ALTER USER 'express1'@'localhost' IDENTIFIED WITH mysql_native_password BY 'xxxxx';
**grant all on express_rent.* to express1@localhost;**
DB,テーブルの準備
express1ユーザでの処理
-- 親テーブル
create table role (
role_id TINYINT,
role_name varchar(10) not null,
PRIMARY KEY (role_id)
)DEFAULT CHARSET=utf8;
create table user (
user_id int,
user_name varchar(20) not null,
role_id TINYINT not null,
user_pass CHAR(60) not null,
index role_id_index(role_id),
foreign key fk_user_id (role_id)
references role(role_id),
PRIMARY KEY (user_id)
)DEFAULT CHARSET=utf8;
create table payment_status (
payment_status_id TINYINT,
status_name varchar(10) not null,
PRIMARY KEY (payment_status_id)
)DEFAULT CHARSET=utf8;
create table payment_check_status (
payment_check_id TINYINT,
status_check_name varchar(10) not null,
PRIMARY KEY (payment_check_id)
)DEFAULT CHARSET=utf8;
-- 子テーブル
create table payment (
payment_id int auto_increment,
user_id int,
foreign key fk_user_id (user_id)
references user(user_id),
payment_month date not null,
payment_price int not null,
payment_price_comment varchar(100),
payment_status_id TINYINT not null default 2,
foreign key fk_payment_status_id (payment_status_id)
references payment_status(payment_status_id),
payment_day datetime,
payment_comment varchar(100),
payment_check_id TINYINT not null default 2,
foreign key fk_payment_check_id (payment_check_id)
references payment_check_status(payment_check_id),
payment_check_day datetime,
payment_check_comment varchar(100),
PRIMARY KEY (payment_id)
)DEFAULT CHARSET=utf8;
-- 上記のテーブル作成ではダメであった。CONSTRAINTをつけないとダメみたいでした。
-- 子テーブル
create table payment (
payment_id int auto_increment,
user_id int,
CONSTRAINT mytest1
foreign key fk_user_id (user_id)
references user(user_id),
payment_month date not null,
payment_price int not null,
payment_price_comment varchar(100),
payment_status_id TINYINT not null default 2,
CONSTRAINT mytest2
foreign key fk_payment_status_id (payment_status_id)
references payment_status(payment_status_id),
payment_day datetime,
payment_comment varchar(100),
payment_check_id TINYINT not null default 2,
CONSTRAINT mytest3
foreign key fk_payment_check_id (payment_check_id)
references payment_check_status(payment_check_id),
payment_check_day datetime,
payment_check_comment varchar(100),
PRIMARY KEY (payment_id)
)DEFAULT CHARSET=utf8;
データの挿入
-- roleデータの挿入
insert into role values (1, '親');
insert into role values (2, '子供');
-- userデータの挿入
insert into user values (1, 'OOO', 1, "xxxxxxxxxxxxxxx");
insert into user values (2, 'OOO', 2, "xxxxxxxxxxxxxxx");
-- payment_statusの挿入
insert into payment_status values (1, '完了');
insert into payment_status values (2, '未完了');
-- payment_check_statusの挿入
insert into payment_check_status values (1, '完了');
insert into payment_check_status values (2, '未完了');
-- paymentの挿入
-- 2020年分のレコードを挿入する
insert into payment
(user_id, payment_month, payment_price)
values
(2, '2020-01-01', 40000),
(2, '2020-02-01', 40000),
(2, '2020-03-01', 40000),
(2, '2020-04-01', 40000),
(2, '2020-05-01', 40000),
(2, '2020-06-01', 40000),
(2, '2020-07-01', 40000),
(2, '2020-08-01', 40000),
(2, '2020-09-01', 40000),
(2, '2020-10-01', 40000),
(2, '2020-11-01', 40000),
(2, '2020-12-01', 40000);
サーバサイドの準備
# フォルダの作成
cd ~/rentApp/server/
mkdir express_mysql
cd express_mysql/
# 必要なモジュールのインストール
sudo apt install nodejs
sudo apt install npm
npm init -y
npm install express
npm install nodemon
npm install mysql
npm install bcrypt
app.jsは元々作られてないので、自分で作成する。
const express = require('express')
const app = express()
const port = 8010
const bodyParser = require('body-parser');
const bcrypt = require('bcrypt');// パスワードの暗号化
// urlencodedとjsonは別々に初期化する
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(bodyParser.json());
const mysql = require('mysql')
const { response } = require('express')
const con = mysql.createConnection({
host: 'localhost',
user: 'express1',
password: 'xxxxx',
database: 'express_rent',
timezone: 'jst'//これを追加すると、日本時間で取得できる
})
con.connect(function(err){
if (err) throw err;
console.log('Connected')
})
app.get('/', (req, res) => res.send('Hello World!'))
app.post('/login', (req, res) => {
console.log(req.body);
console.log("Received POST Data!");
const password = req.body.user_pass;
const user_name = req.body.user_name;
const sql = `
select user_pass, user_id, role_id from user where user_name like "${user_name}";`;
console.log(sql);
con.query(sql, function (err, result, fields) {
if (err) {
console.log("Error!!!!")
throw err; // Rethrow non-MySQL errors
}
console.log(result);
console.log(result.length);
//console.log(result[0].user_pass);
if (result.length === 0){// SQLの結果が何もない場合、つまりUser名が一致しなかった時
console.log("not user or password");
res.json({
msg: "Fail Login! user name is invalid",
user_id: 0,
role_id: 0
})
} else {
console.log("not undefined");
let check_pass_result = bcrypt.compareSync(password, result[0].user_pass);
console.log(check_pass_result);
if (check_pass_result){// check_pass_result = trueの場合
res.json({
msg: "Success Login!",
user_id: result[0].user_id,
role_id: result[0].role_id
})
} else {
res.json({
msg: "The login attempt failed. Either the user ID or password is invalid.",
user_id: 0,
role_id: 0
})
//res.send("The login attempt failed. Either the user ID or password is invalid.");
}
}
});
});
app.get('/main', (req, res) => {
console.log('GETパラメータuser_id取得: ' + req.query.user_id);
console.log('GETパラメータrole_id取得: ' + req.query.role_id);
if (req.query.role_id == 0){
res.send("Please Push Main.");
}
else if (req.query.role_id == 1){// 親の場合
const sql = `
select
payment_id,
payment_month,
payment.user_id,
user.user_name,
payment_price,
case
when payment_status_id = 1 and payment_check_id = 2 then "チェック待ち"
when payment_status_id = 1 and payment_check_id = 1 then "完了"
else "未完了"
end payment_now_status
from payment
inner join user on payment.user_id = user.user_id
order by payment_month`;
con.query(sql, function (err, result, fields) {
if (err) throw err;
res.json(result)
console.log(result[0].payment_id)
});
}else{// 親以外の場合
const sql = `
select * from
(select
payment_id,
payment_month,
payment.user_id,
user.user_name,
payment_price,
case
when payment_status_id = 1 and payment_check_id = 2 then "チェック待ち"
when payment_status_id = 1 and payment_check_id = 1 then "完了"
else "未完了"
end payment_now_status
from payment
inner join user on payment.user_id = user.user_id
order by payment_month)
AS tmp
where user_id = ${req.query.user_id}`;
con.query(sql, function (err, result, fields) {
if (err) throw err;
res.json(result)
console.log(result[0].payment_id)
});
}
})
app.get('/price', (req, res) => {
const sql = `
select
payment_month,
payment_price,
payment_price_comment
from payment
order by payment_month;`;
con.query(sql, function (err, result, fields) {
if (err) throw err;
res.send(result)
});
})
// ToDo: ログインユーザだけの結果をとってくる
app.get('/payment', (req, res) => {
const sql = `
select
payment_id,
payment_month,
payment_price,
payment_price_comment,
payment_status.status_name,
payment_day,
payment_comment,
payment_check_status.status_check_name,
payment_check_day,
payment_check_comment
from payment
inner join payment_status on payment.payment_status_id = payment_status.payment_status_id
inner join payment_check_status on payment.payment_check_id = payment_check_status.payment_check_id
order by payment_month;`;
con.query(sql, function (err, result, fields) {
if (err) throw err;
res.send(result)
});
})
app.post('/update/payment', (req, res) => {
console.log(req.body);
console.log("Received POST Data!");
const payment_id = req.body.id;
const payment_status_id = req.body.payment_status_id;
const payment_comment = req.body.payment_comment;
const sql = `
update payment
set payment_status_id = ${payment_status_id},
payment_day = now(),
payment_comment = "${payment_comment}"
where payment_id = ${payment_id};`;
console.log(sql);
con.query(sql, function (err, result, fields) {
if (err) throw err;
});
res.send("Received POST Data!");
});
app.post('/update/check', (req, res) => {
console.log(req.body);
console.log("Received POST Data!");
const payment_id = req.body.id;
const payment_check_id = req.body.payment_check_id;
const check_comment = req.body.check_comment;
const sql = `
update payment
set payment_check_id = ${payment_check_id},
payment_check_day = now(),
payment_check_comment = "${check_comment}"
where payment_id = ${payment_id};`;
console.log(sql);
con.query(sql, function (err, result, fields) {
if (err) throw err;
});
res.send("Received POST Data!");
});
app.listen(port, () => console.log(`Example app listening on port ${port}!`))
Macでコーディングしていた。
app.jsをscpでMacからラズパイに送るのが一番早そう。
今見返すと、コード結構雑です。
サーバを起動させる
npx nodemon app.js
# 下のコマンドの方がいいかも?
nohup node app.js &
ブラウザ上でJSONが表示されることを確認する
フロントエンドの準備
Vue CLIの準備
cd ~/rentApp/frontend/
sudo npm install -g @vue/cli
# vue create my-project
vue create my-project-raspi
# cd my-project
cd my-project-raspi
npm install --save axios vue-axios
/srcのコードを全てMac上のコードに変更する
main.js
import Vue from 'vue'
import App from './App.vue'
import axios from 'axios' //追記
import VueAxios from 'vue-axios' //追記
Vue.config.productionTip = false
Vue.use(VueAxios, axios) //追記
new Vue({
render: h => h(App),
}).$mount('#app')
App.vue
(めちゃくちゃ長い。読みにくいコードが出来上がっていた。。。あと、Vue.jsは勉強不足ですので、良さを十分に発揮できている気がしません。。。)
<template>
<div>
<header>
<h1>Rent App</h1>
<p v-if="login_user_id != 0">
<!--<a href="#" @click="main()">main</a>-->
<a href="#" @click="logout()">ログアウト</a>
</p>
</header>
<ul>
<!--
<div v-if="login_user_id == 0">
<li><a href="#" @click="login()">login</a></li>
</div>
<div v-else>
<li><a href="#" @click="price()">price</a></li>
<li><a href="#" @click="payment()">payment</a></li>
<li><a href="#" @click="logout()">logout</a></li>
</div>
-->
</ul>
<!-- APIからデータが取れているか確認する用
<p>{{ results[0] }}</p>
<div v-for="(value, key) in results" :key="key">
{{ value }}
</div>
<ul v-for="(value, key) in results" :key="key">
<li>{{ value }}</li>
<li>{{ value.payment_month }}</li>
</ul>
-->
<!--変数の確認用
<h1>ページ:{{type}}</h1>
<h2>ユーザID:{{login_user_id}}</h2>
<h2>ユーザID:{{login_role_id}}</h2>
-->
<div v-if="login_user_id != 0" class="login-header">
<div class="box"></div>
<p>ユーザ名: {{user_name}}<br><a href="#" @click="main()">状況一覧を表示する</a></p>
</div>
<div v-if="login_user_id == 0"><!--ログインユーザがいない時-->
<!--ログインフォーム-->
<h1 class="login-title">My Family<br>Rent Management</h1>
<div class="button-form">
<input v-model="user_name" placeholder="ユーザ名"><br>
<input v-model="user_pass" placeholder="パスワード"><br>
<button v-on:click="login_post()">ログイン</button>
</div>
</div>
<div v-else-if="type == 'main'">
<table class="table">
<thead>
<tr>
<th>支払年月</th>
<th>状況</th>
<!--
<th>詳細確認</th>
-->
<th>更新</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in results" :key="key" :class="{'not-finish': value.payment_now_status === '未完了', 'wait-check': value.payment_now_status === 'チェック待ち', finish: value.payment_now_status === '完了'}">
<td @click="payment(value.payment_id)">{{ value.payment_month }}<br>{{ value.user_name }}</td>
<td @click="payment(value.payment_id)">{{ value.payment_price }}<br>{{ value.payment_now_status }}</td>
<!--<td class="table-payment"><a href="#" @click="payment(value.payment_id)">詳細</a></td>-->
<td class="table-edit"><a href="#" @click="edit(value.payment_id)">更新</a></td>
</tr>
</tbody>
</table>
</div>
<div v-else>
<div v-if="type == 'price'">
<table class="table">
<thead>
<tr>
<th>payment_month</th>
<th>payment_price</th>
<th>payment_price_comment</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in results" :key="key">
<td>{{ value.payment_month }}</td>
<td>¥ {{ value.payment_price }}</td>
<td>{{ value.payment_price_comment }}</td>
</tr>
</tbody>
</table>
</div>
<transition name="fade">
<div v-if="type == 'payment'">
<div class="wrapper">
<div v-for="(value, key) in results" :key="key" class="payment">
<div v-if="value.payment_id == now_payment_id">
<tr>
<th>支払年月</th><th>{{ value.payment_month }}</th>
</tr>
<tr>
<th>金額</th><th>¥{{ value.payment_price }}</th>
</tr>
<tr>
<th>金額コメント</th><th>{{ value.payment_price_comment }}</th>
</tr>
<tr>
<th>支払い状況</th><th>{{ value.status_name }}</th>
</tr>
<tr>
<th>支払い日</th><th>{{ value.payment_day }}</th>
</tr>
<tr>
<th>支払いコメント</th><th>{{ value.payment_comment }}</th>
</tr>
<tr>
<th>チェック状況</th><th>{{ value.status_check_name }}</th>
</tr>
<tr>
<th>チェック日</th><th>{{ value.payment_check_day }}</th>
</tr>
<tr>
<th>チェックコメント</th><th>{{ value.payment_check_comment }}</th>
</tr>
<!--
<p>支払年月:{{ value.payment_month }}</p>
<p>金額:¥{{ value.payment_price }}</p>
<p>金額コメント:{{ value.payment_price_comment }}</p>
<p>支払い状況:{{ value.status_name }}</p>
<p>支払い日:{{ value.payment_day }}</p>
<p>支払いコメント:{{ value.payment_comment }}</p>
<p>チェック状況:{{ value.status_check_name }}</p>
<p>チェック日:{{ value.payment_check_day }}</p>
<p>チェックコメント:{{ value.payment_check_comment }}</p>
-->
<br>
<a href="#" @click="main()">← 戻る</a>
</div>
</div>
</div>
<!--
<table class="table">
<thead>
<tr>
<th>payment_month</th>
<th>status_name</th>
<th>payment_day</th>
<th>payment_comment</th>
<th>status_check_name</th>
<th>payment_check_day</th>
<th>payment_check_comment</th>
</tr>
</thead>
<tbody>
<tr v-for="(value, key) in results" :key="key">
<td>{{ value.payment_month }}</td>
<td>{{ value.status_name }}</td>
<td>{{ value.payment_day }}</td>
<td>{{ value.payment_comment }}</td>
<td>{{ value.status_check_name }}</td>
<td>{{ value.payment_check_day }}</td>
<td>{{ value.payment_check_comment }}</td>
</tr>
</tbody>
</table>
-->
</div>
</transition>
<div v-if="type == 'edit'">
<!--子供ユーザの場合-->
<div v-if="login_role_id == 2">
<div class="wrapper">
<div v-for="(value, key) in results" :key="key" class="edit">
<div v-if="value.payment_id == now_payment_id">
<!-- now_payment_idに、クリックしたレコードのpayment_idが入っている -->
<tr>
<th>支払い年月</th><th>{{ value.payment_month }}</th>
</tr>
<tr>
<th>ユーザ</th><th>{{ value.user_name }}</th>
</tr>
<tr>
<th>金額</th><th>¥{{ value.payment_price }}</th>
</tr>
<tr>
<th>支払い状況</th>
<th>
<input type="radio" id=1 value=1 v-model="picked" checked="checked">
<label for="one">完了</label>
<input type="radio" id=2 value=2 v-model="picked">
<label for="two">未完了</label>
</th>
</tr>
<tr>
<th>支払いコメント</th>
<th>
<textarea v-model="comment" placeholder="コメントがあれば記入してください"></textarea>
<p style="white-space: pre-line;">{{ message }}</p>
</th>
</tr>
<tr>
<td td colspan="2" align="center">
<button v-if="picked != 0" v-on:click="payment_post()">更新</button>
<button v-if="picked == 0">更新(支払い状況を選択して下さい)</button>
</td>
</tr>
<br>
<br>
<a href="#" @click="main()">← 戻る</a>
</div>
</div>
</div>
</div>
<!--親ユーザの場合-->
<div v-if="login_role_id == 1">
<div class="wrapper">
<div v-for="(value, key) in results" :key="key" class="edit">
<div v-if="value.payment_id == now_payment_id">
<!-- now_payment_idに、クリックしたレコードのpayment_idが入っている -->
<tr>
<th>支払い年月</th><th>{{ value.payment_month }}</th>
</tr>
<tr>
<th>ユーザ</th><th>{{ value.user_name }}</th>
</tr>
<tr>
<th>金額</th><th>¥{{ value.payment_price }}</th>
</tr>
<tr>
<th>支払い状況</th><th>{{ value.payment_now_status }}</th>
</tr>
<tr>
<th>チェック状況</th>
<th>
<input type="radio" id=1 value=1 v-model="picked">
<label for="one">完了</label>
<input type="radio" id=2 value=2 v-model="picked">
<label for="two">未完了</label>
</th>
</tr>
<tr>
<th>チェックコメント</th>
<th>
<textarea v-model="comment" placeholder="コメントがあれば記入してください"></textarea>
<p style="white-space: pre-line;">{{ message }}</p>
</th>
</tr>
<tr>
<td td colspan="2" align="center">
<button v-if="picked != 0" v-on:click="check_post()">更新</button>
<button v-if="picked == 0">更新(支払い状況を選択して下さい)</button>
</td>
</tr>
<br>
<br>
<a href="#" @click="main()">← 戻る</a>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
data () {
return {
msg: 'Hello World!',
results: [],
type: "",
// 更新フォームの値を保存する
now_payment_id: 0,
picked: 0,
comment: "",
// ログインするときの情報を格納するところ
user_name: "",
user_pass: "",
// ログイン時のユーザ情報を格納する
login_user_id: 0,
login_role_id: 0
}
},
methods: {
clear() {
this.msg = ''
this.axios.get("/main")
.then(res=>{
console.log(res.data);
this.results = res.data;
//alert(res.data);
//alert(typeof(res.data))
})
.catch((e) => {
//alert(e);
console.log(e)
});
},
main() {
//alert("call main")
this.msg = ''
this.axios.get("/main", {
params: {
// ここにクエリパラメータを指定する
user_id: this.login_user_id,
role_id: this.login_role_id
}
})
.then(res=>{
console.log(res.data);
this.results = res.data;
this.type = "main";
//alert(res.data);
//alert(typeof(res.data))
})
.catch((e) => {
//alert(e);
console.log(e)
});
},
price() {
this.msg = ''
this.axios.get("/price")
.then(res=>{
console.log(res.data);
this.results = res.data;
this.type = "price";
//alert(res.data);
//alert(typeof(res.data))
})
.catch((e) => {
//alert(e);
console.log(e)
});
},
payment(payment_id) {
this.msg = ''
this.now_payment_id = payment_id
this.axios.get("/payment")
.then(res=>{
console.log(res.data);
this.results = res.data;
this.type = "payment";
//alert(res.data);
//alert(typeof(res.data))
})
.catch((e) => {
//alert(e);
console.log(e)
});
},
edit(payment_id) {
this.msg = ''
this.type = "edit";
this.now_payment_id = payment_id
},
payment_post(){
this.type = "post";
// postの操作をする
this.axios.post('/update/payment', {
id: this.now_payment_id,
payment_status_id: this.picked,
payment_comment: this.comment
})
.then(function (response) {
alert(response.data);
console.log(response.data);
}).catch(function (error) {
alert(error.data);
console.log(error);
});
//this.type = "main";
this.main();
},
check_post(){
this.type = "post";
// postの操作をする
this.axios.post('/update/check', {
id: this.now_payment_id,
payment_check_id: this.picked,
check_comment: this.comment
})
.then(function (response) {
alert(response.data);
console.log(response.data);
}).catch(function (error) {
alert(error.data);
console.log(error);
});
//this.type = "main";
this.main();
},
login() {
this.msg = ''
this.type = "login";
},
login_post(){
this.type = "login";
// postの操作をする
let self = this
this.axios.post('/login', {
user_name: this.user_name,
user_pass: this.user_pass
})
.then(function (response) {
alert(response.data.msg);
console.log(response.data);
console.log(response.data.user_id);
console.log(response.data.role_id);
console.log(typeof(response.data.user_id))
self.login_user_id = response.data.user_id;
self.login_role_id = response.data.role_id;
}).catch(function (error) {
alert(error.data);
console.log(error);
});
//console.log(this.login_role_id);
//this.login_user_id = self.login_user_id
//this.login_role_id = self.login_role_id
//this.type = "main";
console.log(this.login_user_id)
console.log(this.login_role_id)
this.$set(this.login_user_id, self.login_user_id)//これでデータが更新されるみたい。
this.$set(this.login_role_id, self.login_role_id)
console.log("setting ok")
this.main()
console.log(this.type)
},
logout() {
this.login_user_id = 0;
this.login_role_id = 0;
this.type = "login";
},
}
}
</script>
<style>
/* スマホでテキスト入力時に拡大してしまう問題を解決するためにフォントサイズを指定する。 */
input{
font-size:16px;
}
textarea{
font-size:16px;
width:100px;
}
/* header */
header {
background-color: #B5D2F3;
font-family: 'Arial',sans-serif;
display: flex;
width: 100%;
}
header h1{
margin-right: auto;
margin-left: 20px;
}
header p{
padding: 10px 30px;
}
header a{
color: black;
}
header a:hover{
color: blue;
}
/* login */
.login-title{
font-family: 'Bodoni 72',sans-serif;
text-align: center;
}
.button-form{
width: 80%;
margin: 0 auto;
}
.button-form input {
font: 15px/24px sans-serif;
box-sizing: border-box;
width: 100%;
height: 40px;
margin: 8px 0;
padding: 0.3em;
transition: 0.3s;
border: 1px solid #1b2538;
border-radius: 4px;
outline: none;
}
.button-form input:focus {
border-color: blue;
}
.button-form button{
font: 15px/24px sans-serif;
box-sizing: border-box;
width: 100%;
height: 40px;
margin: 8px 0;
padding: 0.3em;
transition: 0.3s;
border: 1px solid #1b2538;
border-radius: 4px;
outline: none;
background-color: #B5D2F3;
}
/* login後に付け加えるヘッダー */
.login-header{
margin: 20px;
display: flex;
}
.login-header .box{
background-color:#77b9f7;
width:50px;
height:50px;
border-radius: 10px;
}
.login-header p{
margin-left: 20px;
}
.login-header a{
background-color: whitesmoke;
color: black;
}
/* main */
table {
border-collapse: collapse; /* セルの線を重ねる */
width: 100%;
text-align: left;
}
tr{
border-color: gray;
border-style: solid;
border-width: 1px 0;
z-index: 2;
}
/* mainテーブルのヘッダー以外のコンテンツ部分 */
.not-finish{
background-color: #06067C;
color:yellow;
}
.wait-check{
background-color: #1C80F4;
color:white;
}
.finish{
background-color: #D9EEFF;
}
tbody tr:hover{
box-shadow: 5px;
}
/*
tr:nth-child(odd) {
background-color: #ddd;
}
*/
th,td {
padding: 5px 10px; /* 余白指定 */
}
.table-payment a{
color: black;
border-radius: 4px;
border: 2px solid;
background: rgb(254, 244, 228);
padding: 5px 5px;
text-decoration: none;/* 下線を消す */
}
.table-edit a{
color: black;
border-radius: 4px;
border: 2px solid;
background: rgb(254, 244, 228);
padding: 5px 5px;
text-decoration: none;/* 下線を消す */
z-index: 1;
}
/* payment詳細表示 */
/*
.fade-enter {
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}
.fade-enter-active {
transition: opacity 1s;
}
*/
.wrapper {
max-width: 80%;
margin: 0 auto;
text-align: center;
}
.payment{
display: inline-block;
text-align: left;
}
/* 更新フォームの表示 */
.edit{
display: inline-block;
text-align: left;
}
.edit button{
font: 15px/24px sans-serif;
box-sizing: border-box;
width: 100%;
height: 40px;
margin: 8px 0;
padding: 0.3em;
transition: 0.3s;
border: 1px solid #1b2538;
border-radius: 4px;
outline: none;
background-color: #B5D2F3;
}
</style>
サーバの起動
nohup npm run serve &
動作確認
スマホで動いていることを確認した。
ログイン画面だけですが、画像はこんな感じです。
開発してみた感想
終了した後の気づき
- Vue.jsのDateの動きが予想と違った。
- ページが移動したら、dateも更新されて欲しいのに、更新されてないとか
- 非同期に更新する部分が少し理解が難しかった。
- 無理にSPAを導入する必要もないのかなと思った。
- 個人で制作する物だったら、CSSとかのアニメーションでも十分かと思った。
- コードがすごい雑になっていそう
- レビューしてもらえる人がいるって大切なことなんだと思った。
不足しているところ
- セキュリティを意識した設計
- SQL文とか
- パスワードの暗号化とか
- Gitで途中経過の実装を管理すること
- 途中で、あの昨日どうやったっけ?とかなりがち
- Gitをうまく使っていかないと
まとめ
開発の目的
- 家賃管理アプリを作り、家族の家賃支払い状況を家族全員が確認可能である状態を作ること
- 自分のオリジナルアプリを作り、システム開発の流れを経験を通して理解すること
- 勉強ではっきりイメージできてなかったフロントエンドとバックエンドの役割の違いを理解すること
学び
- 家賃管理アプリを家族へ提供
- 家族の家賃管理アプリを作成することができ、現在も家族に使ってもらっています。使ってもらえるものを作っていると、結構楽しいですね。
- 開発の流れの理解
- ざっと開発の流れを個人的に学べた気がしました。要件の整理や、画面設計やDBのテーブル設計などを事前にドキュメントして保存しておくことで、コーティング作業が作業がスムーズにできました。また、これを大人数でやる際には、意思疎通が大変だろうな、と想像してました。
- フロントエンドとバックエンドの役割の違い。
- フロントエンドとバックエンドを分けることで、フロントエンドは画面に表示に集中、バックエンド側ではデータの提供に集中、と役割が別れていると思いました。両者のアクセス方法だけ決めておけば、別々に開発ができそうなのが、分けて考えることの良さかと思いました。
改善点
- エラー
- 思いもしないエラーが結構発生しています。ログを取って、エラー内容をSlackで通知通知するなど、仕組みを作りたいなと思いました。
- フロントエンド
- SPAの良さをあまりいかせてない気がしました。そこまでUIに拘らなくてもいいなら、個人で開発するレベルならjQueryなどで簡単に組み込むくらいでもいいのかなとも感じました。
- コーディング
- 他の人も読みやすい、また拡張を考慮したコーディングにはなっていないと思います。
- 途中で、あの昨日どうやったっけ?とかなりがちでした。Gitをうまく使っていかないと。
- セキュリティを意識した設計
- SQL文とか、パスワードの暗号化とか。勉強していきます。
- ...
最後に
初めての投稿記事を記載してみて、ちゃんと記載しようとすると結構大変であると感じた一方で、自分のやったことを振り返るのは良いことかと思いました。
まだまだ初心者レベルではありますが、また何か開発などの際には、記事を記載してみようと思います。