目的
同意書の印刷・署名という作業が煩雑で、つい飼主様から頂き忘れてしまうことがある。
印刷した紙の保存が面倒。まとめてスキャンも手間。
市販の署名システムは高価で小規模診療所には割に合わない。
何か近代的な同意書を作りたかった。
待合室で同意書をご記入頂き、提出してお預かりという流れにしたかった。
作成は基本chatGPT
プロンプトは「私は動物病院を経営しています。ipadに同意書を表示して飼い主に手書きサインをもらい、その同意書をpdfにしてローカルの他PCに転送できるブラウザソフトを作りたいです。 具体的には同意書は、カルテ番号フォームや自動で入る日付フォーム、飼主名とペット名フォーム、いくつかの典型文から選択やその都度入力の注意事項を表示して、下に署名欄を作りたいです。 pdf化したらその署名は画像形式に変換され編集不可となる様にします。 送信ボタンを押すとpdfがローカルの他PCのデスクトップ上のフォルダに、自動的に日付が入り送られるシステムを作りたいのですが教えていただけますでしょうか。」
Node.jsサーバー準備
1.保存用PCにNode.jsをインストールする
https://nodejs.org/ (LTSとnpmをダウンロード)
ダウンロードした.msiを実行、基本全て「次へ」
Tools for Native Modulesはチェック入れたままでOK
コマンドプロンプトにて以下を入力してバージョンが表示されれば成功
npm -v
C:\consent-system
そのディレクトリの中にpdfフォルダ(pdfの保存先となる)、publicフォルダ(htmlやsignature.jsを入れる)を新規作成
consent-systemフォルダをShift+右クリック→「ここでPowerShell」
初期化コマンドとして以下を入力するとディレクトリ内にpackage.jsonが作成される。
const express = require(“express”);
const app = express();
app.use(express.json({ limit: “10mb” }));
app.get(“/”, (req, res) => {
res.send(“同意書サーバー起動中”);
});
app.listen(3000, () => {
console.log(“サーバー起動:http://localhost:3000”);
});
以下のコマンドを入力してサーバー起動と出れば成功
iPadで表示する署名htmlを作成
1.以下のタグでhtmlを作成して、publicファイルに入れる
<!DOCTYPE html>
<html lang=”ja”>
<head>
<meta charset=”UTF-8″>
<title>同意書 署名</title>
<style>
body {
font-family: sans-serif;
}
#signature {
border: 2px solid #000;
width: 100%;
max-width: 600px;
height: 200px;
touch-action: none; /* iPadで重要 */
}
button {
margin-top: 10px;
font-size: 16px;
}
</style>
</head>
<body>
<h3>署名欄</h3>
<canvas id=”signature”></canvas><br>
<button onclick=”clearSignature()”>署名を消す</button>
<button onclick=”submitSignature()”>送信</button>
<script src=”signature.js”></script>
</body>
</html>
更にsignature.jsというファイルをpublicフォルダに作成して以下をコピペする
const canvas = document.getElementById(“signature”);
const ctx = canvas.getContext(“2d”);
let drawing = false;
// Canvasの実サイズを指定(重要)
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width;
canvas.height = rect.height;
ctx.lineWidth = 2;
ctx.lineCap = “round”;
ctx.strokeStyle = “#000”;
}
resizeCanvas();
window.addEventListener(“resize”, resizeCanvas);
// 座標取得(iPad対応)
function getPosition(e) {
if (e.touches && e.touches.length > 0) {
return {
x: e.touches[0].clientX – canvas.getBoundingClientRect().left,
y: e.touches[0].clientY – canvas.getBoundingClientRect().top
};
} else {
return {
x: e.clientX – canvas.getBoundingClientRect().left,
y: e.clientY – canvas.getBoundingClientRect().top
};
}
}
// 描画開始
function startDrawing(e) {
drawing = true;
const pos = getPosition(e);
ctx.beginPath();
ctx.moveTo(pos.x, pos.y);
e.preventDefault();
}
// 描画中
function draw(e) {
if (!drawing) return;
const pos = getPosition(e);
ctx.lineTo(pos.x, pos.y);
ctx.stroke();
e.preventDefault();
}
// 描画終了
function stopDrawing(e) {
drawing = false;
e.preventDefault();
}
// イベント登録
canvas.addEventListener(“mousedown”, startDrawing);
canvas.addEventListener(“mousemove”, draw);
canvas.addEventListener(“mouseup”, stopDrawing);
canvas.addEventListener(“mouseleave”, stopDrawing);
canvas.addEventListener(“touchstart”, startDrawing);
canvas.addEventListener(“touchmove”, draw);
canvas.addEventListener(“touchend”, stopDrawing);
// 署名クリア
function clearSignature() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
// PNG画像として取得して送信
function submitSignature() {
const imageData = canvas.toDataURL(“image/png”);
fetch(“/submit”, {
method: “POST”,
headers: {
“Content-Type”: “application/json”
},
body: JSON.stringify({
signature: imageData
})
})
.then(res => res.text())
.then(msg => alert(msg));
}
server.jsの完成
既存のserver.jsを以下に書き換え
const express = require(“express”);
const fs = require(“fs”);
const path = require(“path”);
const { PDFDocument, StandardFonts } = require(“pdf-lib”);
const app = express();
app.use(express.json({ limit: “20mb” }));
// PDF保存先
const PDF_DIR = path.join(__dirname, “pdf”);
// 受信エンドポイント
app.post(“/submit”, async (req, res) => {
try {
const {
chartNo = “未入力”,
ownerName = “未入力”,
petName = “未入力”,
note = “”,
signature
} = req.body;
// 日付
const today = new Date();
const dateStr = today.toISOString().slice(0, 10);
// PDF作成
const pdfDoc = await PDFDocument.create();
const page = pdfDoc.addPage([595, 842]); // A4
const font = await pdfDoc.embedFont(StandardFonts.Helvetica);
let y = 780;
// テキスト描画
page.drawText(“同 意 書”, { x: 240, y, size: 18, font });
y -= 40;
page.drawText(`日付:${dateStr}`, { x: 50, y, size: 12, font });
y -= 20;
page.drawText(`カルテ番号:${chartNo}`, { x: 50, y, size: 12, font });
y -= 20;
page.drawText(`飼い主名:${ownerName}`, { x: 50, y, size: 12, font });
y -= 20;
page.drawText(`ペット名:${petName}`, { x: 50, y, size: 12, font });
y -= 40;
page.drawText(“【注意事項】”, { x: 50, y, size: 12, font });
y -= 20;
const noteLines = note.split(“\n”);
noteLines.forEach(line => {
page.drawText(line, { x: 50, y, size: 11, font });
y -= 15;
});
// 署名画像処理
const base64 = signature.replace(/^data:image\/png;base64,/, “”);
const pngBytes = Buffer.from(base64, “base64”);
const pngImage = await pdfDoc.embedPng(pngBytes);
const pngDims = pngImage.scale(0.5);
page.drawText(“署名:”, { x: 50, y: 200, size: 12, font });
page.drawImage(pngImage, {
x: 120,
y: 120,
width: pngDims.width,
height: pngDims.height
});
// PDF保存
const fileName = `${dateStr}_カルテ${chartNo}_${ownerName}_${petName}.pdf`;
const pdfPath = path.join(PDF_DIR, fileName);
const pdfBytes = await pdfDoc.save();
fs.writeFileSync(pdfPath, pdfBytes);
res.send(“PDFを保存しました”);
} catch (err) {
console.error(err);
res.status(500).send(“PDF作成エラー”);
}
});
// 起動
app.listen(3000, () => {
console.log(“同意書サーバー起動:http://localhost:3000”);
});
あ


