Next.jsを使って患者管理アプリを作ってみます。
今回は患者情報の編集にかかるCRUD処理の実装。
できあがり
環境構築
Next.jsの環境構築は特別なことしてません。
package.json
{
"name": "next-15",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-radio-group": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-tabs": "^1.1.2",
"better-sqlite3": "^11.7.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.471.0",
"next": "15.1.4",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"sqlite3": "^5.1.7",
"tailwind-merge": "^2.6.0",
"tailwindcss-animate": "^1.0.7"
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@types/better-sqlite3": "^7.6.12",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"@types/sqlite3": "^3.1.11",
"eslint": "^9",
"eslint-config-next": "15.1.4",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
}
}
今回ハマったのは一旦登録した患者情報の編集(PATCH)処理についてでした。
UIの実装
患者情報を入力するformと、登録した患者情報の一覧が表示されるTableは以下のように実装しました。
app/patients/page.tsx
<Card className="w-[600px] mx-auto">
<CardHeader className="py-4">
<CardTitle className="text-center bg-gray-300 py-3 rounded">患者情報の登録</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit}>
<div className="grid w-full items-center gap-4">
<div className="flex flex-col space-y-1.5">
<Label htmlFor="patientname" className="font-semibold text-orange-500">患者氏名</Label>
<Input
id="patientname"
type="text"
placeholder="患者 太郎"
value={patientname}
onChange={(e) => setPatientname(e.target.value)}
/>
</div>
<div className="flex gap-4 ">
<div className="flex items-center gap-4 basis-1/3">
<Label htmlFor="affected-side" className="font-semibold text-orange-500">受傷側</Label>
<RadioGroup
defaultValue="right"
className="flex gap-4"
value={affectedside}
onValueChange={(e) => setAffectedside(e)}>
<div className="flex items-center space-x-2">
<RadioGroupItem value="右" id="right" />
<Label htmlFor="right">右</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="左" id="left" />
<Label htmlFor="left">左</Label>
</div>
</RadioGroup>
</div>
<div className="flex items-center basis-2/3">
<Label htmlFor="affected-part" className="basis-1/4 font-semibold text-orange-500">受傷部位</Label>
<Select
value={affectedpart}
onValueChange={(e) => setAffectedpart(e)}
>
<SelectTrigger id="affected-part" className="basis-3/4">
<SelectValue placeholder="選択"/>
</SelectTrigger>
<SelectContent position="popper">
<SelectItem value="母指">母指</SelectItem>
<SelectItem value="示指">示指</SelectItem>
<SelectItem value="中指">中指</SelectItem>
<SelectItem value="環指">環指</SelectItem>
<SelectItem value="小指">小指</SelectItem>
<SelectItem value="手関節">手関節</SelectItem>
<SelectItem value="尺骨">尺骨</SelectItem>
<SelectItem value="橈骨">橈骨</SelectItem>
<SelectItem value="手指">手指</SelectItem>
<SelectItem value="肘">肘</SelectItem>
</SelectContent>
</Select>
</div>
</div>
<div className="flex items-center gap-4">
<Label htmlFor="diagnosis" className="basis-1/6 font-semibold text-orange-500">診断</Label>
<Input
id="diagnosis"
type="text"
placeholder="診断名を入力"
value={diagnosis}
name="diagnosis"
onChange={(e) => setDiagnosis(e.target.value)}
/>
</div>
</div>
<Button className="mt-8 w-full bg-blue-500 font-bold py-4" type="submit">登録</Button>
</form>
</CardContent>
</Card>
app/patients/page.tsx
<div className="flex flex-col items-center gap-5 mt-10 ">
<div className="p-6 bg-gray-300 w-[600px] rounded">
<h2 className="text-center font-bold bg-gray-50 py-2">登録患者一覧</h2>
<Table>
{ patients.length === 0 ? (
<TableCaption>患者が登録されていません</TableCaption>
) : ""}
<TableHeader>
<TableRow>
<TableHead className="w-[80px] text-center">患者ID</TableHead>
<TableHead className="w-[100px]">患者氏名</TableHead>
<TableHead colSpan={2}>診断</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{/* ここでmapメソッドで出力 */}
{ patients.map( (patient) => (
<TableRow key={`patient${patient.id}`}>
{editingId === patient.id ? (
<>
{console.log(patient)}
<TableCell className="patient-id text-center" aria-disabled key={`id-${patient.id}`}>{patient.id}</TableCell>
<TableCell className="patient-name">
<input
type="text"
name="patientname"
value={editingFormData.patientname}
onChange={handleEditFormChange}
/>
</TableCell>
<TableCell className="patient-diagnosis flex flex-col gap-2" key={`params-${patient.id}`}>
<input
className=""
type="text"
name="affectedside"
value={editingFormData.affectedside}
onChange={handleEditFormChange}
/>
<input
type="text"
name="affectedpart"
value={editingFormData.affectedpart}
onChange={handleEditFormChange}
/>
<input
type="text"
name="diagnosis"
value={editingFormData.diagnosis}
onChange={handleEditFormChange}
/>
</TableCell>
<TableCell className="flex justify-end gap-4">
<Button
className="bg-green-600 text-white rounded px-5 py-2 font-bold"
onClick={saveEdit}
>
保存</Button>
</TableCell>
</>
) : (
// 編集モードではない時
<>
<TableCell className="patient-id text-center">{patient.id}</TableCell>
<TableCell className="patient-name text-center">{patient.patientname}</TableCell>
<TableCell className="patient-diagnosis">
{patient.affectedside}
{patient.affectedpart}
{patient.diagnosis}
</TableCell>
<TableCell className="flex justify-end gap-4">
<Button
className="bg-green-600 text-white rounded px-5 py-2 font-bold hover:bg-white hover:text-green-500 hover:border"
onClick={() => startEditing(patient)}
>
編集</Button>
<Button
className="bg-red-600 text-white rounded px-5 py-2 font-bold"
onClick={() => confirmDelete(patient.id)}
>
削除</Button>
</TableCell>
</>
)}
</TableRow>
))}
</TableBody>
</Table>
</div>
</div>
クライアント側での処理
app/patients/page.tsx
//関数の中で記述
const [patients, setPatients] = useState<Patients[]>([]);
const [patientname, setPatientname] = useState("");
const [affectedside, setAffectedside] = useState("");
const [affectedpart, setAffectedpart] = useState("");
const [diagnosis, setDiagnosis] = useState("");
// 編集時に必要
const [editingId, setEditingId] = useState<number | null>(null);
const [editingFormData, setEditingFormData] = useState({patientname: "" ,affectedside: "", affectedpart: "", diagnosis: ""});
// フォーム送信を行った時のアクション
const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
// APIにPOSTリクエストする
const res = await fetch("../../api/patients/", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({patientname, affectedside, affectedpart, diagnosis}),
})
// リクエストに成功した時はjsonオブジェクトに変換して、必要情報をsetPatientsで入れる
if(res.ok){
const data = await res.json();
console.log("患者情報を作成:", data);
setPatients((prevPatients) => [...prevPatients, data]);
setPatientname("");
setAffectedside("");
setAffectedpart("");
setDiagnosis("");
} else {
// リクエストが無効の場合
console.error("患者情報の作成に失敗")
}
}
const handleDelete = (id: number) => {
fetch(`../api/patients/?id=${id}`, {
method: "DELETE",
})
.then((res) => res.json())
.then((data) => {
if(data.success){
setPatients((prevPatients) => prevPatients.filter((patient) => patient.id !== id));
} else{
console.error("削除失敗", data.error)
}
})
.catch((err) => console.error(err))
}
const confirmDelete = (id: number) => {
if (window.confirm('このユーザーを削除しますか?')) {
handleDelete(id);
window.location.href = "/patients"
}
}
// 編集モードの開始
const startEditing = (patient: Patients) => {
setEditingId(patient.id);
setEditingFormData(
{
patientname: patient.patientname,
affectedside: patient.affectedside,
affectedpart: patient.affectedpart,
diagnosis: patient.diagnosis
});
}
// 編集フォームの値を変更
const handleEditFormChange = ((e: React.ChangeEvent<HTMLInputElement>) => {
const {name, value} = e.target;
setEditingFormData((prevData) => ({...prevData,[name]: value}));
});
// 編集を保存
const saveEdit = async () => {
try{
console.log("saveEditの開始")
const response = await fetch(`../api/patients/?id=${editingId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(editingFormData)
});
console.log("レスポンスを受信:",response);
if(response.ok){
const updatePatient = await response.json();
console.log("editingId", editingId)
setPatients(prevPatients =>
(prevPatients.map(p => (p.id === editingId ? updatePatient : p)))
);
setEditingId(null);
console.log("editingId", editingId)
// setPatientname("");
setEditingFormData({
patientname: "",
affectedside: "",
affectedpart: "",
diagnosis: "",
})
// setPatients()
console.log(typeof editingFormData)
console.log("saveEditを終了します:", editingFormData)
console.log(patients)
} else {
console.error("データの更新に失敗しました")
}
} catch(error) {
console.error("更新中にエラーが発生しました:", error)
}
}
// useEffectを利用して初期値
useEffect(()=> {
const fetchData = async () => {
try{
const res = await fetch("../../api/patients/");
console.log("getリクエスト2")
if(!res.ok){
throw new Error("データの取得に失敗しました")
}
const data = await res.json();
if(Array.isArray(data)){
setPatients(data);
} else {
console.error("予期しないデータ形式:", data)
}
} catch(e) {
console.error("データ取得エラー:",e);
}
};
fetchData();
}, [])
サーバー側での実装
app/api/patients/route.ts
import { Database } from "sqlite3";
import path from "path";
import { NextRequest } from "next/server";
// SQLiteデータベースを設定
const dbPath = path.resolve(process.cwd(), "database.sqlite");
const db = new Database(dbPath);
// 初回起動時にテーブルを作成
db.serialize(() => {
db.run(`
CREATE TABLE IF NOT EXISTS patients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
patientname TEXT NOT NULL,
affectedside TEXT NOT NULL,
affectedpart TEXT NOT NULL,
diagnosis TEXT NOT NULL
)
`, (err) => {
if(err) {
console.error(`テーブル作成時にエラーが起きました:${err}`)
} else {
console.log("患者テーブルの作成に成功しました")
}
})
});
// GET関数でクライアントからのリクエストを処理
export async function GET(request: NextRequest) {
// どんなリクエストがあったかログ出力しておく
console.log("GETリクエストがありました",request.body);
// Promiseオブジェクトを返すことで、クライアントサイドでデータを参照できる
return new Promise<Response>((resolve, reject) => {
// データベースの該当テーブルを取得
db.all("SELECT * FROM patients", [], (err, rows) => {
if(err){
console.error(`Database Error: ${err}`);
reject(new Response(JSON.stringify({error: "DatabaseのGETエラー"}), {status: 500}))
} else {
resolve(new Response(JSON.stringify(rows), {status: 200, headers: {"Content-Type": "application/json"}}));
}
})
})
}
// POST関数でサーバーへのレスポンス返却について処理
export async function POST(request:NextRequest) {
// どんなリクエストなのかログに残す
console.log(request.body);
// 取得した内容をJSONオブジェクトにして定数に渡す
const {patientname, affectedside, affectedpart, diagnosis} = await request.json();
return new Promise<Response>((resolve, reject) => {
db.run(
'INSERT INTO patients (patientname, affectedside, affectedpart, diagnosis) VALUES (?, ?, ?, ?)',
[patientname, affectedside, affectedpart, diagnosis],
function (err) {
if(err){
reject(new Response(JSON.stringify({error: "データベースエラー"}), {status: 500}))
} else {
resolve(new Response(JSON.stringify({
id: this.lastID,
patientname,
affectedside,
affectedpart,
diagnosis
}), {status: 201, headers: {"Content-Type": "application/json"}}))
}
})
})
}
// DELETE関数で患者テーブルにある情報を削除する
export async function DELETE(request: NextRequest){
console.log(request.body);
const {searchParams} = new URL(request.url);
const id = searchParams.get("id");
return new Promise<Response>((resolve, reject) => {
if(!id){
resolve(new Response(JSON.stringify({error: "IDが含まれていません"}), {status: 400}));
return;
}
db.run("DELETE FROM patients WHERE id = ?", [id], function(err){
if(err){
reject(new Response(JSON.stringify({error: "データベースのエラーです"}), {status: 500}))
} else {
resolve(new Response(JSON.stringify({success: true}), {status: 200}))
}
})
})
}
// PATCH関数でデータの編集を行う
export async function PATCH(request: NextRequest){
const {patientname, affectedside, affectedpart, diagnosis} = await request.json();
const {searchParams} = new URL(request.url);
const id = searchParams.get("id");
return new Promise<Response>((resolve, reject) => {
db.run(`
UPDATE patients
SET patientname = ?, affectedside = ?, affectedpart = ?, diagnosis = ?
WHERE id = ?
`,
[patientname, affectedside, affectedpart, diagnosis, id],
function(err){
if(err){
reject(new Response(JSON.stringify({error: "更新しようとしましたがデータベースのエラーが発生"}), {status: 500}))
} else {
resolve(new Response(JSON.stringify({id, patientname, affectedside, affectedpart, diagnosis}), {status: 200, headers: {"Content-Type": "application/json"}}))
}
}
)
})
}
PATCH関数の実装部分で、db.run
した後にSQL文でWHERE id = ?
を書いてなくて値が変更されなかったり
[patientname, affectedside, affectedpart, diagnosis, id]
の部分でidが抜けていたがためにデータ更新すると全てのデータが編集したデータに置き換わってしまったりといったエラーが生じていました。
基本の流れはこれでできたと思うので、あとはどんどん応用させていくだけですな。