【第2回】Wikiテーブルの追加とダッシュボードの改修

wiki管理アプリ作成

こんにちは。前回は、Next.js で開発したパスワード管理アプリに Codex を活用して Wiki 機能を追加した内容をご紹介しました。今回は、Wiki のテーブル構成とその実装コード、ダッシュボードの改修点について詳しくご紹介します。

パスワード管理の記事は以下を参照してください。

【第1回】なぜ自分でパスワード管理アプリを作ったのか?〜40代エンジニアの試行錯誤〜

【第1回】の記事は以下を参照してください。

【第1回】なぜWiki機能を追加しようと思ったのか?〜40代エンジニアがCodeXに挑む理由〜

SQLiteデータベースのテーブル追加

以下の SQL を使って、既存の password_manager テーブルに加え、新たに wiki テーブルを作成しました(Codex によって生成されたコードです)。

    
import db from '../src/lib/db';

db.exec(`
  CREATE TABLE IF NOT EXISTS password_manager (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    site_name TEXT NOT NULL,
    site_url TEXT NOT NULL,
    login_id TEXT,
    password TEXT NOT NULL,
    email TEXT,
    memo TEXT,
    category TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
  );
  CREATE TABLE IF NOT EXISTS wiki (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
  );
`);
console.log('パスワード管理テーブルとwikiテーブルを作成しました');
    
 

ダッシュボードの改修

Top ページのダッシュボードには以下のような改修を加えました:

  • 最新の Wiki 項目を上部に表示
  • 「Wiki 登録」ボタンを追加
  • /wikis への一覧リンクを追加

これにより、パスワード情報と Wiki 情報を一元的に確認できるようになり、使いやすさが大きく向上しました。

/src/app/(main)/page.tsx

    
'use client';

import { useCallback, useEffect, useState } from 'react';
import Link from 'next/link';
import PasswordList from '../components/PasswordList';
import WikiCards from '../components/WikiCards';
import type { Password } from '@/types/password';
import type { Wiki } from '@/types/wiki';

const MainPage = () => {
  const [passwords, setPasswords] = useState<Password[]>([]);
  const [wikis, setWikis] = useState<Wiki[]>([]);
  const [loading, setLoading] = useState(true);
  const [errors, setErrors] = useState<{ diaries?: string; wikis?: string; passwords?: string }>({});

  const fetchData = useCallback(async <T,>(
    url: string,
    setter: (data: T) => void,
    key: keyof typeof errors
  ) => {
    try {
      const response = await fetch(url);
      if (!response.ok) throw new Error(`${key}の取得に失敗しました。`);
      const data: T = await response.json();
      setter(data);
    } catch (err) {
      console.error(`Error fetching ${key}:`, err);
      setErrors((prev) => ({ ...prev, [key]: (err as Error).message }));
    }
  }, []);

  useEffect(() => {
    const loadData = async () => {
      await Promise.all([
        fetchData<Password[]>('/api/passwords', setPasswords, 'passwords'),
        fetchData<Wiki[]>('/api/wiki?limit=3', setWikis, 'wikis'),
      ]);
      setLoading(false);
    };
    loadData();
  }, [fetchData]);

  if (loading) {
    return <div>読み込み中...</div>;
  }

  return (
    <div className="p-3">
      <header className="flex justify-between items-center">
        <h1 className="text-2xl font-bold">パスワード管理</h1>
        <div className="flex space-x-4">
          <Link
            href="/wikis/new"
            className="bg-blue-500 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:bg-blue-600 active:scale-95 transition-transform"
          >
            Wiki登録
          </Link>
          <Link
            href="/passwords/new"
            className="bg-green-500 text-white font-bold py-2 px-4 rounded-lg shadow-md hover:bg-green-600 active:scale-95 transition-transform"
          >
            パスワード登録
          </Link>
        </div>
      </header>
      <section className="my-6">
        <h2 className="text-xl font-semibold">最新Wiki</h2>
        {errors.wikis ? (
          <p className="text-red-500">{errors.wikis}</p>
        ) : wikis.length > 0 ? (
          <WikiCards wikis={wikis} />
        ) : (
          <p className="text-gray-500">登録されたWikiがありません。</p>
        )}
        <div className="mt-2">
          <Link href="/wikis" className="text-blue-600 hover:underline">
            一覧を見る
          </Link>
        </div>
      </section>
      <section className="my-6">
        <h2 className="text-xl font-semibold">パスワード一覧</h2>
        {errors.passwords ? (
          <p className="text-red-500">{errors.passwords}</p>
        ) : passwords.length > 0 ? (
          <PasswordList passwords={passwords} />
        ) : (
          <p className="text-gray-500">登録されたパスワードがありません。</p>
        )}
      </section>
    </div>
  );
};

export default MainPage;
    
 

WikiCards コンポーネントの作成

Wiki 記事をカード型で表示するため、Tailwind CSS を用いて視認性の高い UI を構築しました。

/src/app/components/WikiCards.tsx

    
'use client';
import type { Wiki } from '@/types/wiki';
import Link from 'next/link';

type Props = { wikis: Wiki[] };

const WikiCards: React.FC<Props> = ({ wikis }) => (
  <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
    {wikis.map((wiki) => (
      <div key={wiki.id} className="border rounded p-4 bg-white shadow">
        <h3 className="font-bold mb-2 truncate">{wiki.title}</h3>
        <p className="line-clamp-3 text-sm whitespace-pre-wrap mb-2">{wiki.content}</p>
        <Link href={`/wikis/${wiki.id}`} className="text-blue-600 hover:underline">詳細</Link>
      </div>
    ))}
  </div>
);

export default WikiCards;

    
 

TypeScriptの型定義

Wiki データは以下のように型定義しました。データの整合性を保ちつつ、型推論によって開発効率が向上します。

/src/types/wiki.d.ts

    
export type Wiki = {
  id: number;
  title: string;
  content: string;
  created_at: string;
};
    
 

APIエンドポイントの実装

Codex により自動生成された、Wiki API の GET / POST 処理は以下のとおりです。Codex の支援により、迅速かつ安全な実装が可能になりました。

/src/app/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 });
  }
}
    
 

次回予告:「Wiki記事の登録・更新機能を実装」

次回は、Wiki 記事の新規登録フォームと既存記事の更新処理の実装について紹介します。今回と同様、ゆるっとしたペースで進めていきますので、ぜひお楽しみに。

まとめ

今回は、Wiki テーブルの追加と Top ページのダッシュボード改修についてご紹介しました。OpenAI Codex の活用により、コード生成の速度と精度が向上し、より多くのリソースをアイデアの実装に割けるようになりました。

今後も Codex を活かした開発事例を共有していきますので、ご期待ください。

コメント

0 件のコメント:

コメントを投稿

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