
AIツールを用いて全国のフリーランスや、日々勉強している人々が気軽につながれるサービスを作成する本企画。これまでにChatGPTを用いた要件定義から利用環境の選定、SupabaseとNext.jsを連携する作業を進めてきました。
前回の記事「【Supabase × Next.js】プロジェクトの認証・投稿機能を実装する方法!」ではアカウント登録やログインなどの認証機能や、投稿機能を実装する所まで進める内容でした。
今回はその続きとして、日本地図と都道府県のUI実装と都道府県別の投稿ページ、ナビゲーションメニューを実装する過程について解説します。
今回ChatGPTとCursorで実装を進めて完成した状態が以下の通りです。
上記の状態まで実装する過程を紹介します
今回ChatGPTの出力コード量が多いため、各コードをフォルダにまとめています。もしコード全体を確認したい方は、こちらをクリックしてソースコードをダウンロードして下さい。
地図のUIを作成する
地図のUIをどのように作るのかChatGPTへ聞いてみます。

日本地図UIの作り方から順番にわかりやすく教えて下さい。

日本地図UIの実装 - 順番に進める手順
1. SVG日本地図の準備
まず、日本地図のSVGファイルを準備しましょう。
手順 1-1: 画像ディレクトリを作成
mkdir -p public/images
手順 1-2: SVG日本地図を準備
以下のSVGコードを public/images/japan-map.svg
として保存します。
<?xml version="1.0" encoding="UTF-8"?>
<svg width="800px" height="650px" viewBox="0 0 800 650" version="1.1" xmlns="http://www.w3.org/2000/svg">
<title>日本地図</title>
<g id="Japan-Map" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<!-- 北海道 -->
<path id="hokkaido" d="M617,74 L640,113 L659,129 L658,148 L686,164 L698,193 L678,228 L670,240 L649,243 L637,230 L615,246 L600,262 L577,276 L541,269 L515,243 L529,224 L505,194 L520,149 L547,118 L581,96 L617,74" fill="#D8D8D8" stroke="#979797"></path>
<!-- 東北 -->
<path id="aomori" d="M522,283 L545,272 L578,290 L585,307 L570,318 L540,315 L520,297 L522,283" fill="#D8D8D8" stroke="#979797"></path>
</g>
</svg>
~以下省略~
2. 日本地図コンポーネントの作成
手順 2-1: コンポーネントファイルの作成
mkdir -p app/components
手順 2-2: JapanMap.tsx の実装
以下のコードを app/components/JapanMap.tsx
に保存します:
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
import Image from 'next/image';
// 都道府県の配列(地方ごとにグループ化)
const prefectures = {
hokkaido: ['北海道'],
tohoku: ['青森県', '岩手県', '宮城県', '秋田県', '山形県', '福島県'],
kanto: ['茨城県', '栃木県', '群馬県', '埼玉県', '千葉県', '東京都', '神奈川県'],
chubu: ['新潟県', '富山県', '石川県', '福井県', '山梨県', '長野県', '岐阜県', '静岡県', '愛知県'],
kinki: ['三重県', '滋賀県', '京都府', '大阪府', '兵庫県', '奈良県', '和歌山県'],
chugoku: ['鳥取県', '島根県', '岡山県', '広島県', '山口県'],
shikoku: ['徳島県', '香川県', '愛媛県', '高知県'],
kyushu: ['福岡県', '佐賀県', '長崎県', '熊本県', '大分県', '宮崎県', '鹿児島県', '沖縄県'],
};
// 都道府県と対応するSVGのIDのマッピング
const prefectureIdMap: { [key: string]: string } = {
'北海道': 'hokkaido',
'青森県': 'aomori',
'岩手県': 'iwate',
~以下省略~
3. トップページに地図を統合
手順 3-1: app/page.tsx
の修正
以下のコードを app/page.tsx
に保存して、トップページに日本地図を表示します:
import Link from 'next/link';
import JapanMap from './components/JapanMap';
export default function Home() {
return (
<main className="container mx-auto px-4 py-8">
<section className="text-center mb-10">
<h1 className="text-4xl font-bold mb-4">全国フリーランス・勉強仲間マッチング</h1>
<p className="text-xl text-gray-600 mb-8">
仕事仲間や勉強仲間を見つけて、あなたのスキルを高めましょう
</p>
<div className="flex justify-center gap-4">
<Link
href="/auth"
className="bg-blue-500 hover:bg-blue-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
ログイン / 新規登録
</Link>
<Link
href="/posts"
className="bg-green-500 hover:bg-green-600 text-white px-6 py-3 rounded-lg font-medium transition-colors"
>
投稿を見る
</Link>
</div>
~以下省略~
上記の指示通りに作業を進めていきます。

はじめにpublicフォルダにimagesフォルダを作成し、「japan-map.svg」というファイルを作ってChatGPTに出力されたコードを入力します。

次にComponentsフォルダへ「JapanMap.tsx」ファイルを作成し、同じくChatGPTに指示されたコードを入力。

最後にトップ画面を表示するapp/page.tsxの内容を変更して完了です。ChatGPTの処理に従ってこれらの作業を実行したところ、以下のUIが完成しました。

各都道府県の位置は合っているようですが、形状が微妙ですね。とはいえ右の各都道府県へアクセスできるリンクやカテゴリページへのリンク、ログイン認証など最低限の機能は実装できています。
Next.jsで各投稿ページを作る方法
次に各都道府県の投稿ページを作ります。とはいえ作り方はわかりないので、いつものごとくChatGPTへ丸投げします。

各投稿ページの作り方をわかりやすく教えてください。

4. 都道府県別の投稿ページの作成
手順 4-1: 都道府県ページ用のディレクトリを作成
mkdir -p app/prefecture/[name]
手順 4-2: app/prefecture/[name]/page.tsx
の実装
以下のコードを app/prefecture/[name]/page.tsx
に保存します:
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
// 投稿の型定義
type Post = {
id: number;
created_at: string;
user_id: string;
prefecture: string;
category: string;
type: string;
content: string;
users?: {
name: string;
} | null;
};
export default function PrefecturePage({ params }: { params: { name: string } }) {
const prefectureName = decodeURIComponent(params.name);
~以下省略~
上記の指示通り、コマンドプロンプトで
mkdir -p app/prefecture/[name]
を実行し、その直下に「page.tsx」ファイルを作ってコードを入力します。

ちなみに[name]は動的セグメントと呼ばれており、Next.jsのApp Routerにおけるルーティング構造のひとつです。 今回のケースではこの [name] が都道府県名に応じて変化し、それに応じたページが動的に表示されます。
たとえばユーザーが「URL: /prefecture/Tokyo」にアクセスした場合、URLの "Tokyo" の部分が [name] に当たり、params.name = "Tokyo" という形でコンポーネントに渡されます。
この値を使うことにより、React側でSupabaseから該当する都道府県の投稿データを取得し、その内容を画面に表示します。 つまり [name] を使うことで、都道府県ごとに異なる投稿一覧ページを1つのテンプレートで動的に生成できる、というのがポイントです。

ChatGPTの指示通りにコードを入力すると、上記のようなエラーが発生しました。こういう時はエラーコードをコピーしてそのままChatGPTへ貼り付けます。

「4. 都道府県別の投稿ページの作成」にて、下記のエラーが表示されました。
Console Error
A param property was accessed directly with params.name. params is now a Promise and should be unwrapped with React.use() before accessing properties of the underlying params object. In this version of Next.js direct access to param properties is still supported to facilitate migration but in a future version you will be required to unwrap params with React.use().
warnForSyncAccess
file:///C:/Users/○○○○/Desktop/next-app/next-app/.next/static/chunks/22d2ba..js (920:13)
Object.get
file:///C:/Users/○○○○/Desktop/next-app/next-app/.next/static/chunks/22d2ba..js (896:21)
PrefecturePage
file:///C:/Users/○○○○/Desktop/next-app/next-app/.next/static/chunks/19afc2..js (23:54)
Show ignored frames

Next.js 14の新しいパラメータ処理に対応するための修正
この警告は、Next.js 14で params
の扱いが変更されたことによるものです。 最新のNext.jsでは params
が Promise
になっているため、適切にアンラップする必要があります。
修正方法
より良い方法として、このページをクライアントコンポーネントとサーバーコンポーネントに分けることを検討できます。 以下のようにファイルを2つに分けましょう。
クライアントコンポーネント「prefecture/PrefecturePageClient.tsx」
'use client';
import { useEffect, useState } from 'react';
import Link from 'next/link';
import { supabase } from '@/lib/supabase';
type Post = {
id: number;
created_at: string;
user_id: string;
prefecture: string;
category: string;
type: string;
content: string;
users?: {
name: string;
} | null;
};
export default function PrefecturePageClient({ prefectureName }: { prefectureName: string }) {
const [posts, setPosts] = useState<Post[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchPosts = async () => {
};
fetchPosts();
}, [prefectureName]);
}
(全コード)サーバーコンポーネント「/prefecture/page.tsx」
import PrefecturePageClient from './PrefecturePageClient';
export default function PrefecturePage({ params }: { params: { name: string } }) {
const prefectureName = decodeURIComponent(params.name);
return <PrefecturePageClient prefectureName={prefectureName} />;
}
どうやらNext.jsのバージョン更新により処理方法が変わったようですね。指定通り新しくクライアントコンポーネント用のファイルを作成し、各ファイルごとにコードを入力します。
しかし次は「モジュール './PrefecturePageClient' またはそれに対応する型宣言が見つかりません」というエラーが発生しました。
「PrefecturePageClient」はすでに用意してあるので、パスの問題かと思い修正を試みましたがうまくいきません。ChatGPTへ聞いても解決できなかったので、今度はCursorのAIチャット機能を使って解決を試みます。

エラーが表示されているページで「Command+Shift+L」キーを押すと、Cursorの右側にChat画面が表示されます。そこにエラー内容を入力・実行します。
すると上記のようにCursorのAI機能がエラー解決のためにコードを修正してくれます。

処理内容が英語で表示される場合は、上記のように「日本語で教えてください。」と指示するだけでOK。

修正範囲は上記のようにハイライトで表示されます。修正内容を反映させる場合は「Ctrl + Shift +Y」、修正を反映させたくないときは「Ctrl + N」を実行します。
ファイル全体で修正したコードを反映させる場合は、画面下部の「Accept file」をクリックすると反映されます。

修正したあと投稿ページへアクセスしてみましたが、ずっと「Loading...」画面のままでうまく動いていないようです。こちらの状況についてCursorに解決してもらいましょう。

上記のようにうまくいかない内容をチャットで伝えて解決を試みます。supabaseのpostsテーブルからデータを取得する関数がコメントで省略されていたようです。

Cursorがいい感じに修正してくれたようなので、全ての修正内容を反映させます。

無事Supabaseからデータが取れていますね。デザインは置いといて、これで一端投稿ページの機能が実装完了です。
Next.jsでナビゲーションメニューを作成する
次はヘッダーに掲載するナビゲーションメニューを作成します。

ナビゲーションの実装方法を教えてください。

5. ナビゲーションの追加
手順 5-1: ナビゲーションコンポーネントの作成
touch app/components/Navigation.tsx
手順 5-2: Navigation.tsx
の実装
以下のコードを app/components/Navigation.tsx
に保存します:
'use client';
import Link from 'next/link';
import { useState, useEffect } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { supabase } from '@/lib/supabase';
import { useAuth } from '@/lib/contexts/AuthContext';
export default function Navigation() {
const router = useRouter();
const pathname = usePathname();
const { user } = useAuth();
const [isMenuOpen, setIsMenuOpen] = useState(false);
const handleSignOut = async () => {
await supabase.auth.signOut();
router.push('/');
router.refresh();
};
// モバイルメニューの外側をクリックしたら閉じる
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
const target = event.target as HTMLElement;
if (isMenuOpen && !target.closest('#mobile-menu') && !target.closest('#menu-button')) {
setIsMenuOpen(false);
}
};
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}, [isMenuOpen]);
// ページが変わったらメニューを閉じる
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);
~以下省略~
手順 5-3: ナビゲーションをレイアウトに追加(全コード)
app/layout.tsx
を以下のように修正して、全ページにナビゲーションを追加します:
import './globals.css';
import { AuthProvider } from '@/lib/contexts/AuthContext';
import Navigation from './components/Navigation';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="ja">
<body>
<AuthProvider>
<Navigation />
<main className="min-h-screen bg-gray-50">
{children}
</main>
<footer className="bg-white border-t py-6 text-center text-gray-600">
<p>© 2025 フリーランスマッチング All rights reserved.</p>
</footer>
</AuthProvider>
</body>
</html>
);
}

ChatGPTの指示通りコーディングすることで、このようにヘッダーとナビゲーションメニューを実装できました。これで投稿ページとナビゲーションメニューは作成できましたが、カテゴリーの各ページ作成がまだできていません。
これまでChatGPTに聞いた内容をそのまま実行してきましたが、今回はCursorのAIチャット機能だけでカテゴリページの詳細を実装してみます。
Cursorを使ってNext.jsでカテゴリページを作る

Cursorで「Ctrl + Shift +L」を実行するとAIチャット機能が表示されるので、以下のプロンプトを入力します。
t本プロジェクトでは、Categoryの個別ページが作られていません。各構造の相関性を意識して最適な場所に最適なファイルやフォルダを作成して、Categoryの個別ページをつくれますか?

すると上記のように必要なファイルの作成からコーディングまで実行してくれました。しかしエラーが発生しているので、以下のプロンプトで解決してもらいます。
チャット欄に「Category/page.tsxがエラー表示されているようです。最適な方法で問題を解決してください。」

ファイルをインポートする際、拡張子が指定されていないことからうまくファイルを読み込めていなかったようです。

エラー内容もプロンプトひとつで解決してくれました。最後に「Accept file」をクリックして反映させます。

無事カテゴリの個別ページが表示され、該当する投稿内容を取得できています。今回はここまでとして、次回は各詳細ページのデザインや応募機能を実装予定です。
本記事が参考になった方は、ぜひ次回もチェックしてみて下さい。