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

wiki管理アプリ作成

こんにちは。今回は、個人向けアプリに追加した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 件のコメント:

コメントを投稿

コメントをお待ちしています。