0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Posted at

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

できあがり

image.png

環境構築

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

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

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?