Next.jsで作る患者管理アプリ（患者情報登録編）

Next.jsを使って患者管理アプリを作ってみます。
今回は患者情報の編集にかかるCRUD処理の実装。

## できあがり
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/524273/d7d30b07-9706-038b-188c-5a12392304f1.png)


## 環境構築
Next.jsの環境構築は特別なことしてません。
```json: 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は以下のように実装しました。
```typescript: 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>
```

```typescript: 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>

```

## クライアント側での処理
```typescript: 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();
    }, [])
```

## サーバー側での実装
```typescript: 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が抜けていたがためにデータ更新すると全てのデータが編集したデータに置き換わってしまったりといったエラーが生じていました。

基本の流れはこれでできたと思うので、あとはどんどん応用させていくだけですな。