【第3回】Wiki記事の登録・更新機能を実装する〜個人アプリに柔軟な知識管理を〜
20:00

こんにちは。今回は、個人向けアプリに追加したWiki機能のうち、「記事の登録・更新」機能についてご紹介します。 すべてローカル環境で動作し、OpenAI Codexの助けを借りながら構築したこの仕組みは、「知識を残す」ことのハードルを大きく下げてくれました。
パスワード管理の記事は以下を参照してください。

【第1回】なぜ自分でパスワード管理アプリを作ったのか?〜40代エンジニアの試行錯誤〜
【第1回】の記事は以下を参照してください。

【第1回】なぜWiki機能を追加しようと思ったのか?〜40代エンジニアがCodeXに挑む理由〜
【第2回】の記事は以下を参照してください。

【第2回】Wikiテーブルの追加とダッシュボードの改修
目次
登録機能の実装:app/wiki/new/page.tsx
新しいWiki記事を登録する画面は、以下のような構成です。 タイトルと内容の2つだけというシンプルさが、自分の思考を気軽にメモとして残す上で非常に有効です。
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
const NewWikiPage = () => {
const router = useRouter();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch('/api/wiki', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
if (res.ok) {
router.push('/wikis');
} else {
alert('登録失敗');
}
};
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Wiki作成</h1>
<form onSubmit={handleSubmit} className="space-y-2">
<div>
<label className="block">タイトル</label>
<input value={title} onChange={e => setTitle(e.target.value)} className="w-full border p-2 rounded" required />
</div>
<div>
<label className="block">内容</label>
<textarea value={content} onChange={e => setContent(e.target.value)} className="w-full border p-2 rounded" rows={6} required />
</div>
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">登録</button>
</form>
</div>
);
};
export default NewWikiPage;
更新・削除機能:app/wiki/edit/[id]/page.tsx
次に、記事の更新や削除ができる編集ページです。 「間違えてもすぐに直せる」「もう不要な記事はすぐ消せる」この柔軟さが、日常的に使えるアプリには欠かせません。
'use client';
import { useRouter } from 'next/navigation';
import { useState, useEffect } from 'react';
import type { Wiki } from '@/types/wiki';
const WikiEditPage = ({ params }: { params: { id: string } }) => {
const router = useRouter();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [loading, setLoading] = useState(true);
useEffect(() => {
const load = async () => {
try {
const res = await fetch(`/api/wiki/${params.id}`);
if (!res.ok) throw new Error('読み込み失敗');
const wiki: Wiki = await res.json();
setTitle(wiki.title);
setContent(wiki.content);
} finally {
setLoading(false);
}
};
load();
}, [params.id]);
const handleUpdate = async (e: React.FormEvent) => {
e.preventDefault();
const res = await fetch(`/api/wiki/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title, content }),
});
if (res.ok) {
router.push(`/wikis/${params.id}`);
} else {
alert('更新失敗');
}
};
const handleDelete = async () => {
if (!confirm('削除しますか?')) return;
const res = await fetch(`/api/wiki/${params.id}`, { method: 'DELETE' });
if (res.ok) {
router.push('/wikis');
} else {
alert('削除失敗');
}
};
if (loading) return <div>読み込み中...</div>;
return (
<div className="space-y-4">
<h1 className="text-2xl font-bold">Wiki編集</h1>
<form onSubmit={handleUpdate} className="space-y-2">
<div>
<label className="block">タイトル</label>
<input value={title} onChange={e => setTitle(e.target.value)} className="w-full border p-2 rounded" required />
</div>
<div>
<label className="block">内容</label>
<textarea value={content} onChange={e => setContent(e.target.value)} className="w-full border p-2 rounded" rows={6} required />
</div>
<div className="space-x-2">
<button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded">更新</button>
<button type="button" onClick={handleDelete} className="bg-red-500 text-white px-4 py-2 rounded">削除</button>
</div>
</form>
</div>
);
};
export default WikiEditPage;
バックエンドの処理
API側は、Next.jsのapp/api/wiki以下に、GET / POST / PUT / DELETEをそれぞれ実装しています。
api/wiki/route.ts
import { NextResponse } from 'next/server';
import { runSelect, runExecute } from '@/lib/db';
import type { Wiki } from '@/types/wiki';
export async function GET(request: Request) {
const { searchParams } = new URL(request.url);
const limit = searchParams.get('limit');
try {
const sql = limit
? `SELECT * FROM wiki ORDER BY id DESC LIMIT ?`
: 'SELECT * FROM wiki ORDER BY id DESC';
const results = limit
? runSelect<Wiki>(sql, [Number(limit)])
: runSelect<Wiki>(sql);
return NextResponse.json(results);
} catch (error) {
return NextResponse.json({ error: 'DB取得失敗' }, { status: 500 });
}
}
export async function POST(req: Request) {
try {
const body = await req.json();
const { title, content } = body;
if (!title || !content) {
return NextResponse.json({ error: '必須項目不足' }, { status: 400 });
}
runExecute('INSERT INTO wiki (title, content) VALUES (?, ?)', [title, content]);
return NextResponse.json({ message: '登録成功' });
} catch (error) {
return NextResponse.json({ error: '登録失敗' }, { status: 500 });
}
}
api/wiki/[id]/route.ts
import { NextResponse } from 'next/server';
import { runGet, runExecute } from '@/lib/db';
import type { Wiki } from '@/types/wiki';
export async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
if (!id || isNaN(Number(id))) {
return NextResponse.json({ error: 'Invalid wiki ID.' }, { status: 400 });
}
try {
const result = runGet<Wiki>('SELECT * FROM wiki WHERE id = ?', [Number(id)]);
if (!result) {
return NextResponse.json({ error: 'wiki entry not found.' }, { status: 404 });
}
return NextResponse.json(result);
} catch (error) {
return NextResponse.json({ error: 'Failed to fetch wiki entry.' }, { status: 500 });
}
}
export async function PUT(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
const body = await request.json();
const { title, content } = body;
if (!title || !content) {
return NextResponse.json({ error: 'title and content are required.' }, { status: 400 });
}
runExecute('UPDATE wiki SET title = ?, content = ? WHERE id = ?', [title, content, Number(id)]);
return NextResponse.json({ message: 'wiki entry updated successfully.' });
} catch (error) {
return NextResponse.json({ error: 'Failed to update wiki entry.' }, { status: 500 });
}
}
export async function DELETE(request: Request, { params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
try {
runExecute('DELETE FROM wiki WHERE id = ?', [Number(id)]);
return NextResponse.json({ message: 'wiki entry deleted successfully.' });
} catch (error) {
return NextResponse.json({ error: 'Failed to delete wiki entry.' }, { status: 500 });
}
}
次回予告:「Wiki一覧と詳細画面」
次回は、Wiki一覧と詳細画面の実装について紹介していきます。 今回と同様、ゆるっとしたペースで進めていきますので、ぜひお楽しみに。
まとめ
今回は、個人アプリにおけるWiki記事の「登録」「更新」「削除」機能についてご紹介しました。 どれも小さな機能ではありますが、自分の知識を“あとで見返せる形で残す”という点で、とても重要な一歩です。 次回は、Wikiの「一覧表示」と「詳細画面」の実装を紹介していきます。
0 件のコメント:
コメントを投稿
コメントをお待ちしています。