プログラミンGOO

プログラミングナレッジ、ワードプレス、広告収入等について、気づき・備忘録を残していきます。

Stripe決済でサブスクリプションのクレジットカードを変更したい

機能は豊富だが何かと使い方が複雑なStripe。
今回はすでにクレジットカードでサブスクリプション契約を開始している顧客に『サブスクリプションに使用している決済情報(クレジットカード)を変更したい』と言われた場合の対処方法。
主な流れは以下の通り。

  1. サブスクリプションの支払い方法を一度手動決済(インボイスメールを送付し、メールから決済情報を入力してもらう)に切り替える
  2. 顧客がメールから決済を行い、新しい決済情報が登録される
  3. サブスクリプションの支払い方法を新しい決済(クレジットカード)に変更する

1.サブスクリプションの支払い方法を一度手動決済に切り替える

すでに登録されているクレジットカードではなく、一度手動決済に切り替える。
手動決済とは、インボイスメールを送付し、メールから決済情報を入力してもらう方法のこと。※顧客が一番初めにクレジット登録をする際に対応しているはず

まずは顧客タブに移動して該当の顧客を選択。
顧客選択

顧客ページに移動したら、サブスクリプションのセクションから今回決済情報を変更するサブスクリプションを選択する。
サブスクリプションの選択

サブスクリプションのページに移動したら、右上のアクションボタンから、『サブスクリプションの更新』をクリック。
※これはサブスクリプションの設定を更新するというもので、これを選択しただけでは何も更新されないのでご安心を
サブスクリプションを更新

サブスクリプションの更新ページに移動したら、支払い方法セクションまでスクロールする。
現在使用されている決済(クレジットカード)を、『手動支払い用の請求書を顧客にメールで送付する』に変更する
支払い方法の変更

あとは右上の『サブスクリプションの更新』ボタンで変更を確定して完了。

これで次回のインボイス(請求)は自動引き落としではなく、請求メールが送付される。

2.顧客がメールから決済を行い、新しい決済情報が登録される

顧客が請求メールから新しいクレジットカードで決済を行うと、その決済情報がStripeに登録されるまで、それまで待機する。
決済が行われたら、またStripeから、支払い情報を変更する必要があるため。
普段インボイスが送られている日付を確認するなどして、顧客が決済を行うであろうタイミングを把握しておく。

3.サブスクリプションの支払い方法を新しい決済(クレジットカード)に変更する

顧客がメールから新しいクレジットカードで決済を行ったら、今度はこの決済情報から自動引き落としを行うように設定を変更する。
手順は、1.の逆をやればよい。

ただし、ここで1つ注意点がある。
顧客の支払い情報は、デフォルト設定という設定がされており、これを旧クレジットカードから、今回登録された新規クレジットカードに変更しないと、サブスクリプションの情報を変更するときに登録済みの決済情報に出てこない。
この設定を変更する。
顧客タブ>該当の顧客を選択
顧客選択

顧客のページに移動したら、支払い方法セクションまでスクロールする。
新しいクレジットカード情報が登録されているはずなので、この項目の右側にある三点リーダをクリックし、『デフォルトとして設定』。
デフォルトとして設定

あとは手順1で紹介した方法で決済情報を新しいクレジットカードに変更すれば、次回からは新しいクレジットカードから自動引き落としされるようになる。

コマンドプロンプト(Linuxコマンド)備忘録

以下がよくまとまっているからこっちでいいや
参考:Linux コマンド一覧表 - Qiita

環境

ファイル・フォルダ操作

共通

ファイル・フォルダを移動する

move [ファイル・フォルダ名] [移動先パス]
フォルダ操作

現在のディレクトリを移動する

cd ./パス
cd ../    //1つ上の階層に戻る

カレントディレクトリのパスを取得

pwd

フォルダの一覧を表示

dir

フォルダ作成

mkdir フォルダ名

フォルダコピー

xcopy フォルダ名 移動先パス
ファイル操作

ファイル作成

type ファイル名

ファイルコピー

copy コピー元ファイル名 新しいファイル名

UNIXコマンド備忘録

パワーシェルでやる場合
https://qiita.com/mima_ita/items/ae31f3a19389e69b307f

基本コマンド

dir: 同階層のファイル一覧を表示
Ctrl + l:プロンプトをリセット EscでもOK プロンプトではcls
pwd:現在のディレクトリを表示(print working directory)
cd -:直上のディレクトリに戻る
Ctrl + R →キーワード:キーワードを含む直近のコマンドを検索
history:コマンドの履歴を表示
→!行数:行数のコマンドを実行
!!:直前のコマンドを実行
!-2:2つ前のコマンドを実行
!ab:abから始まる直前のコマンドを実行 ※『:p』を最後に着けると実行せず表示
cd !$:cd ひとつ前のコマンド で実行
mkdir --help:mdkirのヘルプを表示 ※man mkdirでマニュアル
ls:リスト表示
-l:パーミッション・所有者情報も含め
-a:隠しファイル含め

ファイル作成

type nul > ファイル名	windows?

ファイルの内容を閲覧

cat ファイル名

lessコマンド ※ファイルの内容を表示してスクロール可能な状態にする

less ファイル名 //起動
『:』のあとに以下のコマンドで状態を変更
q //終了
/all //文字列『all』を検索

ファイル操作

cp ファイルA ファイルB //AをBにコピー
-r //再帰的にコピー
-f //上書き
-i //上書き確認
mv (ファイルA) (ディレクトリB):移動
mv (ファイルA) (ファイルB):ファイル名変更

rm (ファイル名):削除 ※ディレクトリにも使える?
-r:中身があっても削除
rmdir(ディレクトリ名):削除 ※中身が空の場合のみ

■フォルダ作成

mkdir ディレクトリ名
-p //記載したパスのディレクトリをすべて作成

■ファイル作成

touch ファイル名 //空のファイルを作成(本来は既存ファイルの日付更新)

複数ファイルを作成
参考:ターミナルの作業が捗るかもな小技5つ - Qiita

touch {dir1,dir2}/{file1,file2}

■ファイルのパーミッション変更

chmod a+w //ファイル名

・オプション
a //すべてのユーザ
u //所有者
g //グループ
o //他のユーザ
r:読み、w:書き、x:実行

■ファイル・ディレクトリの所有者とグループを変更

chown ユーザー名 ファイル名

■ディレクトリ内にある文字列を指定して検索

find ディレクトリ -name 文字列

grep (検索文字列) (ファイル名):指定パターンの文字列を検索

■ファイルの編集 :vim
※linuxについてるエディタ
※ファイルがなければ保存した際に新規作成される

vi hello.txt	//vi ●ファイル名●
i	//編集モードに切り替え ※insertの略
Escキー	//編集モード終了
:w	//保存
:q	//vim終了
※編集カ所がある場合はエラーになる。
編集を破棄して終了する場合は
:q!
:wq	//保存して終了
ファイルの内容を確認したい場合は
cat ●ファイル名●	//スクロール可能
または
less ●ファイル名●

・その他コマンド
参考:https://qiita.com/colorrabbit/items/755cfbb0e97d48280775

dd	//行切り取り
p	//貼り付け
.	//直前のコマンドを繰り返す
Ctrl n	//補完
u	//undo
Ctrl r	//redo

【エラー対応ログ】

■warning: LF will be replaced by CRLF
『LFをCRLFに変換するよ』という意味。
LF、CRLFは改行コード

hpでFn(ファンクション)キーが効かないときの対処法

Fn(ファンクション)キーが普段の動作と反転してしまっていたので修正。
ちょっとググったのですが僕のhp(ヒューレットパッカード)のPCと仕様が違うのか解決手順が違ったのでメモ。
ちなみに僕の機種は『EliteBook 830 G8』でした。

他のサイトでよく紹介されているようにBIOS設定で変更するところは一緒です。

Fnキーの挙動修正手順

  1. Shiftキーを押しながら再起動
  2. オプションの選択>トラブルシューティング>詳細オプション>UEFIファームウェアの設定>再起動
  3. BIOS Setup>System Options
  4. Advancedタブに移動
  5. Launch Hotkeys without Fn Keypressを『disable』に設定

postgreSQL備忘録

【参考】
公式サイト
https://www.postgresql.org/

★dkt_searchの初期DB作成についてはSpringSecurityのドキュメントを参照

【管理コマンド基本操作】

・ログイン

psql -U postgres  //管理者 ※本来はユーザ(ロールという)を作成してそちらを使用すること
\c databateName	//データベースに移動 
\dt	//table一覧
\d tableName	//table詳細
select * from TABLENAME;	※ここからコロンが必要なので注意
update tableName set columnName = value where …

・その他のよく使うコマンド

\d	tableName	//table詳細
\l	//db一覧
\q	//quit
\x	//拡張表示on,off
\?	//よく使うコマンド一覧を表示

【DB基本操作】

・値の追加

insert into tableName (columnName, columnName2) values ('clmvalue', 'clm2value'), ('clmvalue2', 'clm2value2');

・抽出

select * from tableName;

・更新

update tableName set columnName = value where …

・行削除

delete from tableName where …

【データ】

数値	: integer(int), real, serial
文字	: char(5), varchar(255), text
真偽	: boolean TRUE FALSE t f
日付	: date, time, timestamp

【構造変更】

■カラム追加

alter table tableName add column columnName dataType;

■カラム削除

alter table tableName drop columnName;

■リネーム

alter table tableName rename to newColumnName;

型変更

alter table tableName alter columnName type newType;

booleanとsmallintなど、扱うタイプが違う場合、『この時はこれ』と条件を明示しないとエラーになる
https://qiita.com/seiro/items/ade4c220dfe4acb0ef4b
※smallintをbooleanに変更

ALTER TABLE users ALTER enabled TYPE boolean
  USING CASE
    WHEN enabled = 1 THEN TRUE
    WHEN enabled = 0 THEN FALSE
    ELSE null
  END;

【テーブル】

テーブル作成

create table tableName (columnName varchar(255), columnName2 text)

名前変更

alter table tableName rename to newTableName

テーブル削除

drop table tableName

【値の取得】クエリ

■基本

select * from tableName where id = 1;

■関連(バインド)

select tableName.columnName, tableName2.columnName2 from tableName, tableName2 where (例) tableName.id = tableName2.foreing_id;

省略形 ※上記と同義

select tn.columnName, tn2.columnName2 from tableName tn, tableName2 tn2 where (例) tn.id = tn2.foreing_id;

【制約】

not null
unique
check(length(columnName) > 5)	//ほかにもいろいろ
default	//例)created timestamp default 'now', is_draft bolean default TRUE
primary key (not null, unique)

・UNIQUE制約について
MySQLはUNIQUE KEYだが
PostgresqlではUNIQUE INDEX またはUNIQUE制約
UNIQUE制約はカラムの制約なので簡単につけ外しはしないので、基本的にはUNIQUE INDEX?
参考:https://okwave.jp/qa/q6326929.html

■外部キー制約
子テーブルのカラムに外部キーを追加することで、親テーブルとの関係性を持たせる。
・外部キーの追加

ALTER TABLE "child" ADD CONSTRAINT "keyName" FOREIGN KEY (parent_id) REFERENCES parent(id) ON DELETE CASCADE;"

childテーブルのparent_idカラムにkeyNameという名前の外部キーを追加する。
このカラムはparentテーブルのidを参照する。
ON DELETE CASCADE:親オブジェクトが変更された場合の処理をどうするかを設定する。この設定では削除処理が行われた場合に子オブジェクトも削除する。

・外部キーの削除

ALTER TABLE "users" DROP COMSTRAINT "users_to_account";

・外部キー制約・参照元の確認

\d tableName

・読み方

外部キー制約:
    "keyName" FOREIGN KEY (client_id) REFERENCES client(id)

親テーブルがあり、そこに向かって外部キー参照をしていることを示す。※自身は子テーブルである
このテーブル(子)のclient_idというカラムに、keyNameという外部キーが設定されているよ。このカラムはclient(親)テーブルのidカラムを参照しているよ。

参照元:
    TABLE "schedule" CONSTRAINT "keyName" FOREIGN KEY (client_id) REFERENCES client(id)

子テーブル(schedule)があり、そこから外部キー参照されていることを表す。※自身は親テーブルである
scheduleテーブル(子)のclient_idカラムから外部キー参照されているよ。外部キーはこのテーブル:clientテーブル(親)のidカラムを参照しているよ。

【インデックス】

インデックスを付けることで大量のデータを処理する際に処理速度を向上させることができる。
インデックスとは付箋。例えば規則性のある商品番号などに設定すると効果的。
逆に、カテゴリーなど、カラムとして管理してしまった方が良いものはインデックスではなくカラムにするほうが適切。
また、インデックスをつけすぎると結局規則性を検索できなくなるため逆効果になることもある。

create index indexName on tableName(columnName);
drop index indexName;	//削除

【ビュー】※よく使うsql文を登録しておく機能

作成

create view viewName as sql;

呼び出し

select * from viewName;

削除

drop view viewName;

ビュー一覧表示

\dv

【トランザクション】

・途中でselect文で確認することもできるよ

開始

begin;

終了

commit;

中止

rollback;

【postgresql管理用コマンド】

・ヘルプ

help

■データベース一覧

psql -l

■データベース作成

createdb dbName

//★このへんはsql文ではなくpostgresqlの管理用コマンドでcreate databace dbNameのラッパー
//なので普通にコマンドプロンプトで実行するとエラーになる。
//その場合は普通にsql文で実行しよう
https://oshiete.goo.ne.jp/qa/2052007.html

※ホスト名の調べ方

ipconfig /all

削除

dropdb dbName

接続

psql dbName

~Mysqlとの違い~
・『`』(バッククォート) :全て不要
・SET AUTOCOMMIT = 0; :\set autocommit = 0FF; ※大文字小文字が区別されるので注意
・tinyint(1) :smallint
・ALTER TABLE `authorities` ADD UNIQUE KEY `username` (`username`,`authority`);
:CREATE UNIQUE INDEX username ON authorities (username,authority);

【appendix】

■バージョン

psql --version

■ユーザの作成
https://qiita.com/wb773/items/248e6e083b2fe12e820a

■外部ファイルからインストール

\i fileName

【導入】

■postgresQLのインストール
参考:https://qiita.com/tom-sato/items/037b8f8cb4b326710f71

※PSQLも一緒にインストールされる
C:\Program Files\PostgreSQL\13\bin のpslq.exe

■パスを通す
C:\Program Files\PostgreSQL\13\bin で登録しておく

■コマンドプロンプト再起動

■バージョン確認

psql -V

■コマンドプロンプトからpsqlを使用
・環境変数のPathを通す
C:\Program Files\PostgreSQL\12\bin
※psql.exeがあるフォルダ

・ログイン

psql -u postgres -d postgres


■パスワードを忘れた
サーバの操作に使用 :pg_ctl
-D :ディレクトリ指定 ※どこを指定すればいいかわからん
https://www.postgresql.jp/document/10/html/app-pg-ctl.html

https://qiita.com/ritya/items/b1ae186f3f6308c52289
こっちは一瞬で出来た

・再起動は以下でできた

pg_ctl restart -D "C:\Program Files\PostgreSQL\12\data"

GUIで再起動する場合 ※これで-Dのパスも調べられた
スタートボタン右クリック>サービスとアプリケーション>サービス(時間かかる)>postgresql
ここでパスとか停止起動で再起動
https://www.dbonline.jp/postgresql/install/index4.html

・原因、なぜか管理者ではなくユーザ名でログインさせられていた。どゆこと…
psql -U postgres で管理者ログイン
パスワードは初期設定したもの。

【エラー対応】

■列●●は存在しません
updateコマンドを実行したがなぜか値が更新できない
update client_media set media_type = “t” where id = 49;
『列tは存在しません』という謎のエラーが出る。

>>SQLでは文字列はシングルクォーテーションで囲うこと

■三角関係を持つテーブル
クライアント>スケジュール、スケジュール>ジャンル、ジャンル>クライアントというような三角関係を持つテーブルをOneToOneで作成したらデータがうまく更新されなかった。
そもそも論だが、テーブル設計において、三角関係になる構造は作らないのが基本らしい。
今回のような処理もそうだが、取れるデータもどこから取るか?で変わってきてしまうことがあるらしい。
参考:データベース設計の際に気をつけていること - 食べチョク開発者ブログ

thymeleaf備忘録

Java+Springboot+thymeleaf

■概要
1.SpringフレームワークでModelインターフェースで受けた値(フィールド)を
HTML(ビュー)側に簡易実装するための仕組み(テンプレートエンジン)

■準備(宣言)
htmlタグに以下を記述する

//urlは特に意味はなくuri(一意)であることを示しているだけ
※これをしなくても動作はするが、STSで認識されないので宣言をしていく
※xmlnsはxml name spaceの略

■留意事項
・thymeleafは入出力を担当するテンプレートツール。
・基本的にはif分岐やリスト操作など、論理演算は内部で済ませ、必要なモノは加工済みの状態で用意しておく。
・viewから送ることができる値はStringのみ。例えば以下のようにオブジェクトを送ろうとしても、Stringとして扱われるため受け取ることができない。

<input type="hidden" name="attrname" th:value="${obj}">

【ビューへの出力】

~Controller~

@GetMapping("/client")
public String createInfo(
    Model model) {

  Object = new object;
  String sample = "sample"
  
  model.addAttribute("example", sample);
  model.addAttribute("obj", obj);
  
  return "client";
}

~view~

<p th:text="${example}"></p>	//exampleはmodelで設定した属性名を指定

オブジェクトのフィールド(プロパティ)を出力する場合

<p th:text="${obj.attribute}"></p>

または

<div th:object="obj">
  <p th:text="*{attribute}"></p>
</div>

【基本】

文字操作

■文字連結(リテラル置換・パイプライン構文)
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#literal-substitutions
以下の2つは同義

<span th:text="|Welcome to our application, ${user.name}!|">
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
配列・リストの扱い

■参考:https://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf_ja.html#%E3%83%9E%E3%83%83%E3%83%97

■値の取り出し
RowMapperなどでDBのリストを作成して出力する場合 :th:each

<tr th:each="inquiry : ${inquiries}" th:object="${inquiry}">	//変数名inquiries : ${ModelにセットされているListオブジェクト}
								//紐づいてinquiryもmodelに登録される?
            <td th:text="*{name}">名前</td>
            <td th:text="*{age}">年齢</td>
            <td th:text="*{gender}">性別</td>
            <td><a th:href="@{|/inquiry/*{id}|}">詳細</a></td>	//後述
</tr>

■リストのn番目の要素を取得
例)Listplansがあり、1番目の要素を取得する。
 ※PlanのフィールドでplanNameがあれば以下のように取得できる。

<input type="text" th:value="${plans.get(1).planName}">


■リストを区切る

th:text="${#strings.listJoin(productList.![id],',')}"

productListのid項目を「,」で区切って並べる
例) productList[x].id -> 「100」「150」「200」
text="100,150,200"

■リストオブジェクトユーティルメソッド

th:if="${#lists.size(listname)}>1"	//『lists』であることに注意
th:if="!${#lists.isEmpty(listname)}"
繰り返し処理 th:each

modelにStringの文字列を格納したリスト"areaNameData"が登録されているとき
リストの文字列一覧を出力したい場合

<span th:each="a : ${areaNameData}">	//areaNameDataの中身を一つずつaに代入 ※aはmodelに格納される?
	<span th:text="${a}"></span>	//aはStringなのでth:textで呼び出し
</span>

・if条件で出しわけたい場合はth:blockでくくる

<th:block th:each="genremap : ${categoriesGenresClientsMap.get('ヒップホップ系')}" >
<tr th:if="${#lists.size(genremap.getValue())} != 0">
	<td th:text="${genremap.getKey()}"></td>
	<td><span th:text="${#lists.size(genremap.getValue())}"></span>校</td>
	<td><input type="checkbox" name="genreNames" th:value="${genremap.getKey()}"></td>
</tr>
</th:block>

■改行やdivなど、ブロック単位で出力する :th:block
空のタグが発生しないのでとても便利

<div th:object="${inquiry}">
  <th:block th:each="line : *{contents.split('\n')}">
	//split():Stringクラスメソッド。
	//inquiryオブジェクトのcontentsを(\n)で分割し
	//変数lineにリストにして格納
	//lineがStringでなくオブジェクトだと.split()は使用できない
    <span th:text="${line}"></span><br>
//その場合は<br>などで調整するしか
  </th:block>
</div>
マップMapの扱い

■th:eachのネスト
https://teratail.com/questions/5133

■mapのメソッド

${map.get(key)}	//キーから値を取得
${#maps.size(mapName)}	//マップの要素数
${#maps.isEmpty(map)}	//空かどうか

//キーや値がマップに含まれているかどうかをチェック
${#maps.containsKey(map, key)}
${#maps.containsAllKeys(map, keys)}
${#maps.containsValue(map, value)}
${#maps.containsAllValues(map, value)}

■実装例
~Java~

Map<Integer, String> SampleMap(
  Model model) {
  
  Map<Integer, String> sampleMapA = new HashMap<>();
  sampleMapA.put(1, "sample");
  sampleMapA.put(2, "sample2");

  Map<Integer, String> sampleMapB = new HashMap<>();
  sampleMapB.put(1, "サンプル");
  sampleMapB.put(2, "サンプル2");

  model.addAttribute("sampleMapA", sampleMapA);
  model.addAttribute("sampleMapB", sampleMapB);
}

~HTML~
//例1

<div th:each="smA : ${sampleMapA}">
  <p th:text="${smA.getKey()}"></p>  //キーを表示:1, 2
  <p th:text="${smA.getValue()}"></p>  //値を表示:"sample", "sample2
</div>

//例2
<div th:each="smA : ${sampleMapA}">
  <div th:each="smB : ${sampleMapB}">
    <p th:if="${smA.getKey()} == ${smB.getKey()}" th:text="${smA.getValue()}"></p>
        //キーが一致したときにsampleMapAの値を表示
  </div>
</div>

//例3
<div th:each="smA, smAStat : ${sampleMapA}">
  <p th:if="${smAStat} == ${#maps.size(sampleMapA)}" th:text="${smA.getValue()}"></p>
    //sampleMapAのStat(何番目に出力されたか)が、
    //sampleMapAの要素数(2)と一致したときに値を表示
</div>

例3のように、MapやListはStat、つまりMapやListが今何番目の要素を表示しているか?といっオブジェクトの情報を使用してロジックを組むこともできる。

■ListをMapにネストする
~Java~

Map<String, List<String>> areaNameWithClientName;	//Map<エリア名, List<エリアに属するクライアント名一覧>>

~HTML~

<div th:each="a : ${areaNameWithClientName}">
  <h3 th:text="${a.getKey()}"></h3>	//Mapは.getKey()でキー名を取得できる
  <table>
    <tr th:each="b : ${a.getValue()}">	//Mapは.getValue()で値を取得できる
                                        //MapのvalueはList<>になっているのでさらにth:each
      <td th:text="${b}"></td>
    </tr>
  </table>
</div>

■th:eachの中でlabel id for

<span th:each="var : arr">
    <input type="checkbox"th:id="${#ids.seq('id_sample')}" th:value="${var.key}" />
    <label th:for="${#ids.prev('id_sample')}" th:text="${var.value}" />
</span>

#ids.prevは#ids.seqを参照して設定される
th:fieldを設定した場合は自動的に#ids.seqが行われるため以下のように書くことができる

<span th:each="var : ${CL_ROLE}">
    <input type="checkbox"th:field="*{rols}" th:value="${var.key}" />
    <label th:for="${#ids.prev('rols')}" th:text="${var.value}" />
</span>
URL・リンク処理

実際のURLはドメインが先頭に入るため、th:href=@{●ULR●}でくくって渡してあげることでthymeleafが補完してくれる。
中に記述するURLはControllerのGetMapで定義しているパスをそのまま記述するものと考えてよさそう。

参考:
https://qiita.com/rubytomato@github/items/ac65c2203d16d1a1bbd7
https://ts0818.hatenablog.com/entry/2017/10/09/144626 //GETではREST形式?

■クエリ形式とREST形式がある。 ※★GETで渡す場合はREST形式が必須??
・クエリ形式:URLパラメータ(『?』の後)として渡す

<a href="/user/profile" th:href="@{/user/profile(id=${'ab123'},role=${'admin'})}">profile</a>
→<a href="/user/profile?id=ab123&amp;role=admin">profile</a>

・REST形式:URL文字列として渡す

<a href="index.html" th:href="@{/user/profile/{id}/{seq}(id=${'abc'},seq=${'1000'})}">profile</a>
→<a href="/user/profile/abc/1000">profile</a>

※前半に{変数名}を指定し、後半の()内で変数に値を代入するテンプレートリテラル
 固定値でなく変数を使用する場合はクオーテーションは不要
 例:変数abc id=${abc} //id=${‘abc’} ではない

■idごとのリンクを作る :th:href ※上記サンプルコード参照

<td><a th:href="@{|/inquiry/*{id}|}">詳細</a></td>	//『|』はパイプライン構文と呼ばれるもの

@{ }の中の文字列であっても${ }や*{ }のように先頭の記号は必要になる。

if分岐

・"${条件式}" 条件式がtrueの場合に表示される
・正反対の挙動を実装したい場合はth:unlessに置き換えるだけで実装できる

<div th:if="${#lists.isEmpty(planDetails)}">※プランが登録されていません</div>
<p th:if="${client.plan} == null">プランが登録されていません</p>
<p th:unless="${client.plan} == null">プランが登録されています</p>

・演算子
以上:ge、 以下:le、 より多きい:gt、 より小さい: lt
等しい:eq、 等しくない:neq
以外:not、
且つ:and、 または:or

<div th:if="${person.intValue ge 20 and person.intValue lt 40}"><img src="/images.png" /></div>
https://arakan-pgm-ai.hatenablog.com/entry/2017/03/14/224244

・演算、リスト、文字列操作

th:if="${#strings.isEmpty(str)}"	//strが空、nullの場合にtrueになる。事前にtrim()されるので空白もtrue。
 th:unless="*{#strings.isEmpty(appeal)}"	//★使いやすい!appealが空でなければ表示
th:if="${a} == ${b}"	//stringの比較など

参考:https://qiita.com/55beagle/items/2208cea678dc948b715c

※th:if以外にこんな書き方ができる
https://www.thymeleaf.org/doc/tutorials/2.1/usingthymeleaf_ja.html#%E7%B9%B0%E3%82%8A%E8%BF%94%E3%81%97%E3%82%B9%E3%83%86%E3%83%BC%E3%82%BF%E3%82%B9%E3%81%AE%E4%BF%9D%E6%8C%81
6.2

th:text="${prod.inStock}? #{true} : #{false}"
演算
th:text="|${#aggregates.sum(productList.![price])}円(+税)|"	//productListのprice項目を合計する(springの記法)
https://qiita.com/55beagle/items/2208cea678dc948b715c

【formの処理】

■th:action
formの送信先を指定するaction属性は、静的に指定しても良いのだが、Springbootでは送信先のURLは可変になることが多いため、基本的には以下のように指定する。

■作成(create)と編集(edit)を同じformで管理したい場合のベストプラクティス

以下のように指定する

<form th:action="@{''}" method="post" th:object="${client}">
  <input type="hidden" th:field="*{id}">	//※1
  …

th:action="@{''}"(自分自身)を指定することで@GetMappingされてきた値でそのままPost送信することができる。(検討th:action=”@{|path|}”)
すなわち、現在のURLがhttps://appName/client/1/createであるとき、th:action=”@{‘’}”でpostすると、action=”/client/{clientId}/create”と読み替えられるため、以下のPostMappingでキャッチできる。

@PostMapping("/client/{clientId}/create")
public String createClient() {
  return "client/form";
}

ただし、※1に記したth:field="*{id}"を渡してあげる必要があるので注意すること

・同じページから相対パスでリクエストを出しわけたい場合
1つのページでcreate、edit、削除を管理したいことがある。その場合は以下のように指定すれば相対パスとなる。

th:action="@{form}"

現在のURLがhttps://appName/client/1/planであるとき、上記はaction=”/client/{clientId}/form"に読み替えられる。※現在のURLの最後のplanがformに置き換わっている点に注意

ただし、このときリクエスト自体は送られるが、遷移するURLが変わってしまう。
例えば、エラー等でページをreturnすると以下のような遷移となり、同じページであるにもかかわらずURLが変わるため、その後のリクエストが正常に送られなくなってしまう。

//元パス
http://page/form.html
//処理成功※redirect
http://page/form.html
//エラー時※return
http://page/form/create.html

・パラメータを送るなどして対応することもできるが、JavaではSPAに対応しておらず、推奨できないと思われる。
作成ページと、編集削除ページは分ける。
※削除は基本的にredirectなので、作成ページまたは編集ページと一緒に実装してもOK

■チェックボックスの表示 ※詳しくはSpringDBマニュアルで
formに出すのは簡単だが、ビューに出力するのみの場合方法がない。
disabled属性を指定する方法もあるが、
th:ifでチェックボックスのture、falseによってメッセージなどを出しわける方法がよさそう。

<div th:object="${client}">
<div th:each="a : ${areaList}"> //選択肢のリストを呼び出し出力
  <input type="checkbox"
  th:text="${a}" //選択肢がテキストで表示される
  name="areaName" //modelのareaNameフィールドに値を登録する
  th:value="${a}" //選択肢をそのままvalueとして設定
			//valueは配列としてpostされる
                  //valueを選択肢の名前と別にしたい場合はListではなくMapにする
  th:checked="${#lists.contains(areaNames, a)}" //表示の際、すでに選択されているものにはチェックを付ける ※後述
  >
</div>
</div>

・th:checked="${#lists.contains(areaNames, a)}"について
リストareaNamesにaが含まれる場合Trueを返す

■ドロップダウンリスト(プルダウン) :select, option

<select name="sampleList">
	<option
	 th:each="a : ${sampleList}"
	 th:value="${a}"
	 th:text="${a}"
	 th:selected="${sampleList} == 'defaultValue'"
	 ></option>
</select>

・デフォルト値の指定 optionを手動で1行追加するだけ

<select name="sampleList">
	<option value="選択してください">選択してください</option>	//デフォルト値を追加
	<option
	 th:each="a : ${sampleList}"
	 th:value="${a}"
	 th:text="${a}"
	 th:selected="${sampleList} == 'defaultValue'"
	 ></option>
</select>

■input type=”time” th:field=”*{time}”
この値はStringとして送られる。modelでデータをTimeなどに指定しているとエラーになる。
これは、Stringをオブジェクトのプロパティとして登録する際にmodelがTime型になっているという型の不一致の扱いとなるため。
type=”text”とすれば良いのだが、type=”time”でルーレットのUIが利用できるので使用したい。
コンバーターを自力で設置するか、アプリケーション内でTime型に書き換える必要がある。
アプリケーション内で書き換える場合は、HTML側ではth:fieldではなくnameで値を取得し、アプリケーション内でキャストする。
どちらも型変換までしてTime型のメリットを享受できるかというと疑問。
modelもString型で処理をしてしまった方が無難。もし、登録した時間を集計したり、Time型のメソッドを使用する場合には型の変換を検討する。

値の受け取り
@PostMapping("/form")
public String example(@RequestParam String genreName) {	//@RequestParamで受け取るが、引数名とパラメータ属性名が同じなら省略可?
}

※ただし、@PathVariable("id")を使用する場合、@ModelAttributeが効かないので、引数にModel.modelを追加、
 model.addAttributeして明示的にMVCモデルにセットする必要がある。

・実装

<form action="/form" method="post" th:object="${obj}">	//model名を指定する
	<input type="text" th:field="*{●属性名●}">
//modelのどのフィールドに登録するか指定する name属性は指定しなくてOK
	//th:field="${obj.●属性名●}"と同義
		//name="●属性名●" th:value="${formClass.●属性名●}"とも同義
</form>

この場合、出力すると以下に置き換わる

<input type="text" id=●属性名● name="●属性名●" value="">
エラー処理 例外処理

@Validでエラーをキャッチしたもの(@Dataのエラー)をthymelefで出力する

エラーメッセージは出力する箇所を

などで作成
メッセージの内容は@Dataの該当オブジェクトで指定する
例)@NotBlank(message="値を入力してください")

・実装 ※th:object="${obj}"でオブジェクトを指定しているブロックの配下であること

<div>
  <span th:if="${#fields.hasErrors('name')}" th:errors="*{name}"></span>
  //"obj"の'name'フィールドで設定しているエラー(@NotBlankなど)が出力される。
 //メッセージを指定している場合はそのメッセージが出力される。
</div>

※springbootのクラスは以下

class="form-text text-muted"

【レイアウト】

テンプレートを階層(レイヤー)化、または部品(フラグメント)化し、共通部分をまとめることで保守性を高めることができる。
※親テンプレートでオブジェクトを使用している場合に、子テンプレートでどのオブジェクトを参照しているのか見えなくなるため、あまり複雑な構造にしすぎないこと

■参考:
https://macchinetta.github.io/server-guideline-thymeleaf/current/ja/ArchitectureInDetail/WebApplicationDetail/TemplateLayout.html
~部品(フラグメント)化する場合~
■部品となる子テンプレートでは、部品にしたい箇所をth:fragmentで括る。

<html
	xmlns:th="http://www.thymeleaf.org/"
>
~~~
<div th:fragment="top-image" th:remove="tag">
	//部品
</div>

※必要なのは部品のみでそれをくくっているdivは必要ないケースがほとんど。
『th:remove="tag"』を付与することで外枠のdivタグは反映されない。

■親テンプレートではth:replaceで呼び出す。

<div th:replace="~{client/fragment/top-image :: top-image}"></div>

※”~{●親テンプレートのパス● :: ●fragment名●}”
親テンプレートのパスとは、templateフォルダ以下の絶対パス
~階層(レイヤー)化する場合~
fragmentのみだと親テンプレートでth:objectを使っていた場合に子テンプレートでもth:objectを呼び出さないといけない。
insertとかreplaceを使用してできるかな…。

レイアウトを使用する場合は親テンプレートにも子テンプレートにも以下を記述し、thymeleafのテンプレート機能を使うことを明示する。

<html
	xmlns:th="http://www.thymeleaf.org/"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>

子テンプレートでは、親テンプレートをのパスを指定する。
例えば親テンプレートのパスがlayoutフォルダ直下の『common.html』である場合は以下のようになる。
※波カッコの前に~が入っていることに注意

<html
	xmlns:th="http://www.thymeleaf.org/"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorate="~{layout/common}"	//追記
>

親テンプレートでは、子テンプレートを埋め込みたい位置に以下のように記述する。

<div layout:fragment="contents"></div>

子テンプレートでも同様の値を使用し、親要素に反映させたい内容を記述する。

<div layout:fragment="contents">
	//親テンプレートに反映させたい内容を記述
</div>

Thymeleaf Layout Dialectモジュールによる共通画面作成............................................................
■導入
Spring Bootのスターターには入っていないため設定を行う
・手順
pom.xmlを開く
依存関係タブ>追加
グループId: nz.net.ultraq.thymeleaf
アーティファクトId: thymeleaf-layout-dialect
pom.xmlを保存

※アプリケーションを実行中の場合は
 設定を反映させるために再実行すること

■共通ページの用意
templateフォルダ直下にlayoutフォルダを追加
layoutフォルダ内にcommon.htmlを作成 (これが共通レイアウトになる)

※index.htmlにメタデータの記述をしてしまわないように注意
あくまでcommon.htmlで設定をするため、BootStrapなどのメタデータはこちらに記述する。
index.htmlも、このcommon.htmlを継承して画面を表示する。

・htmlタグにネームスペースを追記
・必要に応じメタ情報でスタイルシートを適用
 ※BootStrapの設定などはここで記述する
・個別コンテンツを埋め込むカ所を指定する

<html
	xmlns:th="http://www.thymeleaf.org/"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
>
<head>
<meta charset="UTF-8">
<!-- 必要に応じメタ情報を追記 -->
<meta name="viewport" content="width=device-width, initianl-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="/css/bootstrap.min.css">
<title>共通レイアウト</title>
</head>

<body>
<h1 class="m-3">スクール管理ボード</h1>
<nav class="my-3" style="background: azure;">●ナビゲーションバー●</nav>

<!-- 個別コンテンツを埋め込む場所を指定 -->
<div class="container" layout:fragment="contents"></div>

</body>
</html>

■個別ページに共通レイアウトを呼び出す
例)index.htmlに反映させる場合
※共通レイアウトで指定したcssも反映される

<!-- ネームスペースを追加 &使用する共通レイアウトを指定 -->
<html
	xmlns:th="http://www.thymeleaf.org/"
	xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
	layout:decorate="~{layout/common}"
>
<head>
<meta charset="UTF-8">
<title>スクール管理ボード</title>
</head>

<body>

<!-- 個別コンテンツここから -->
<div layout:fragment="contents">

●コンテンツ●

</div>
<!-- 個別コンテンツここまで -->

</body>
</html>

【appendix】

■th:hrefでidのパラメータ(URL)がおかしくなる(文字化け?)
>『|』で囲むこと。上記の文字連結を参照

<a th:href="@{|/client/*{id}/edit|}">

■チェックボックスの値をnullで送信するとエラーになる
MissingServletRequestParameterException
>@RequestParamで値を送信していたが、このアノテーションは必須がデフォルトになっている。
 以下を指定

@RequestParam(required = false)

■Thymeleafのファイルはどこ?
>Maven 依存関係

■#について
https://teratail.com/questions/4676

#{...} :
読み込んであるプロパティでprop.nameのようにアクセスすることが可能

■条件に応じて読み取り専用にする
>|??|
th:readonly="${if条件}"
th:disabled="${if条件}"
|

例)

th:disabled="*{username != null && username != ''}"	//空または何か値があると入力不可(nullのみ許容)

※hasErrors()でreturnすると値がnullから空文字に変わってしまうため上記の条件式を書くことがあった。

【エラー対応】

■layout: fragmentで部品化したコンテンツで、個別にjsファイルを読み込みたいが読み込まない。
>bodyの直前で読み込むのが一般的だが、部品化されるのはlayout: fragmentのdiv内要素のみ。そのためdiv内でjsファイルを読み込む必要がある

<body>
<div layout:fragment="contents" th:object="${client}">
~コンテンツ~
<script type="text/javascript" src="/js/zipsearch.js"></script>
	//読み込まれる
</div>
<script type="text/javascript" src="/js/zipsearch.js"></script>	//読み込まれない
</body>

■th:action=@”{‘’}”が動作しないときのチェックポイント
まず、Controllerがpostをキャッチしているか確認
・していない場合
form送信後のURLを確認し、MappingしているURLと一致するかを調査する
・している場合
returnで返しているURLが合っているか確認する。
良くある間違い return “redirect:form” //相対パスになっている。”redirect:/form”とする

html2canvasでHTMLを画像化する備忘録

今回、スケジュールをオブジェクトで動的に表現しているHTMLのDOMを画像化する要件が発生した。

これを画像化するためにhtml2canvasを使用した。

導入

公式サイトからhtml2canvasの最新版をダウンロードして、使用する。
今回は現状の最新版であるv1.4.1の以下のファイルを選択した。
html2canvas.min.js

このjsを読み込んであげるのみ。
この使いやすさもhtml2canvasの魅力の1つ。

使い方

公式ドキュメントを読めばわかるが、使い方はいたってシンプル。
~HTML~

<div id="target">サンプル<div>

~js~

html2canvas(document.querySelector("#target"), options).then(canvas => {
    document.body.appendChild(canvas)
});

これでtargetのDOMが画像としてbody末尾に挿入される。
optionsにオプションを指定して条件を変えることができる。
optionsの一覧はOptions - html2canvasで確認できる。

例)

html2canvas(document.querySelector("#target"), {
        windowWidth: 1200,
        width: 1200
    }).then(canvas => {
    document.body.appendChild(canvas)
});

画像ダウンロード処理

画像ダウンロードはaタグにダウンロード用のリンクを埋め込むことで実現できる。
しかし、ダウンロードしたい要素は実際に描画されるわけではないのでhrefに設定するパスを静的に取得できない。
html2canvasによって作成されるキャプチャはcanvas要素で、そのURLはcanvas.toDataURL()で取得することができるため、これを利用する。
どこでも良いが、ボタンの後などに隠しリンク(aタグ)を設置し、これにURLを設定してクリックさせる。
~HTML~

<div id="outputBtn" class="btn btn-primary m-3">
  画像表示
</div>
<a id="getImage" href="" style="display: none"></a>

~js~

const outputBtn = document.getElementById("outputBtn");  //ボタン
const element = document.getElementById("schedule-outline");  //画像化したい要素
const getImage = document.getElementById("getImage");  //ダウンロード用隠しリンク

outputBtn.addEventListenner('click', function() {
  html2canvas(element).then(canvas => {
    getImage.setAttribute("href", canvas.toDataURL());
    getImage.setAttribute("download", "sample.png");
    getImage.click();
  });			
})

背景色を透過する

Optinosに以下を指定する

backgroundColor: null;

エラー対応

html2canvas(...).then is not a function

thenは関数じゃないよと言われている。html2cavasのメソッドにthenは存在しないようだ。
調べたところ、html2canvasのバージョンが最新ではないよう。
このとき、古いドキュメントを参照していて、最新バージョンではないhtml2canvasを使用していた。
最新版を入れなおすと解消した。

Widthが効かない

これもhtml2canvasが最新バージョンではなかったことが原因のよう。最新バージョンを入れなおして解消した。

Invalid shorthand property initializer

訳すとプロパティ初期化の省略形違反エラー。
Optionsのプロパティの指定の仕方が間違っている。
【誤】

Width = 100

【正】

Width: 100
画像が見切れる

html2canvasは、デフォルトでは実際にレンダリング(描画)されている範囲しかキャプチャ対象としていない。そのため、ブラウザを小さい幅で表示していればそれに合わたサイズになってしまう。
これを、最大サイズで取得、表示したい場合にはOptionsに以下を設定する。

windowWidth: 1200,    //取得する要素のサイズを指定する
width: 1200    //キャプチャを表示する際に、どのくらいのサイズで表示するかを指定する
画像がぼやける

Optinosでscaleを設定することで解像度を上げることができる

scale: 2

わかりにくければ、一度scale: 0.5とかに設定するとさらにぼやけるので、しっかり解像度が変更されていることがわかる。