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?

簡易工程進捗管理システムの改良⑨~フェーズ5 Next.js フロントエンド作成~

0
Last updated at Posted at 2026-10-04

前の記事

次の記事

フェーズ5-12:エラーハンドリング改善

今回は、APIやDBに接続できない場合にNext.jsの赤いRuntime Errorをそのまま見せず、

データを取得できませんでした。

サーバーとの通信中にエラーが発生しました。
しばらくしてから再度お試しください。

[ 再試行 ]

のような画面を表示する。

Next.js App Routerでは、特殊ファイルerror.tsxを配置すると、そのルート配下で発生した未処理例外を捕捉できる。

error.tsxを作成

src/app/error.tsx

"use client";

import { useEffect } from "react";
import { useRouter } from "next/navigation";

import styles from "./error.module.css";

export default function Error({
  error,
  reset,
}: {
  error: Error & {
    digest?: string;
  };

  reset: () => void;
}) {
  // App Routerを操作するために取得
  const router = useRouter();

  // エラー内容は開発者向けにログへ出力する
  useEffect(() => {
    console.error(
      "画面表示中にエラーが発生しました。",
      error
    );
  }, [error]);

  /**
   * 再試行処理
   *
   * 1. error.tsxのError Boundaryをリセット
   * 2. 現在のルートをサーバーから再取得
   */
  const handleRetry = () => {
    reset();

    router.refresh();
  };

  return (
    <main className={styles.container}>
      <div className={styles.card}>
        <h1 className={styles.title}>
          データを取得できませんでした
        </h1>

        <p className={styles.message}>
          サーバーとの通信中にエラーが発生しました。
        </p>

        <p className={styles.message}>
          しばらくしてから再度お試しください。
        </p>

        <button
          type="button"
          className={styles.retryButton}
          onClick={handleRetry}
        >
          再試行
        </button>
      </div>
    </main>
  );
}

エラー画面用CSS

src/app/error.module.css

/* =========================================
   エラー画面全体
   ========================================= */
.container {
  min-height: 70vh;

  display: flex;

  justify-content: center;
  align-items: center;

  padding: 24px;
}

/* =========================================
   エラーカード
   ========================================= */
.card {
  width: 100%;
  max-width: 520px;

  padding: 32px;

  border: 1px solid #ddd;
  border-radius: 8px;

  background-color: white;

  text-align: center;
}

/* =========================================
   タイトル
   ========================================= */
.title {
  margin-top: 0;
  margin-bottom: 20px;

  font-size: 24px;
}

/* =========================================
   説明文
   ========================================= */
.message {
  margin: 8px 0;

  color: #666;

  line-height: 1.6;
}

/* =========================================
   再試行ボタン
   ========================================= */
.retryButton {
  margin-top: 24px;

  padding: 10px 20px;

  border: none;
  border-radius: 6px;

  background-color: #333;

  color: white;

  font-size: 15px;

  cursor: pointer;
}

/* マウスを乗せた場合 */
.retryButton:hover {
  opacity: 0.85;
}

動作確認

わざとAPIを停止して確認する。
DockerでASP.NET Core APIを停止する。

ブラウザで再試行の画面が表示されることを確認。

その後、APIを起動して、再試行を押すと復旧することを確認。

フェーズ5-13:ローディング表示

Next.jsのloading.tsxは、そのルートのpage.tsxがデータ取得などで待っている間に表示する特殊ファイル。
内部的にはReactのSuspenseを利用している。

これまでは、

ページへ移動
   ↓
getProcesses()
   ↓
API待ち
   ↓
画面完成
   ↓
表示

だったのを、loading.tsxがあると、

ページへ移動
   ↓
┌───────────────────────┐
│ データを読み込んでいます... │
└───────────────────────┘
   ↓
裏でAPI取得
   ↓
取得完了
   ↓
実際のページに切り替え

共通ローディング画面を作る

src/app/loading.tsx

import styles from "./loading.module.css";

/**
 * ページのデータ取得中に表示する画面。
 *
 * Server Componentのレンダリングや
 * APIからのデータ取得を待っている間に表示される。
 */
export default function Loading() {
  return (
    <main className={styles.container}>
      <div className={styles.content}>

        {/* 読み込み中を表すアニメーション */}
        <div className={styles.spinner} />

        <p className={styles.message}>
          データを読み込んでいます...
        </p>

      </div>
    </main>
  );
}

CSSを作成

src/app/loading.module.css

/* =========================================
   ローディング画面全体
   ========================================= */
.container {
  min-height: 70vh;

  display: flex;

  justify-content: center;
  align-items: center;

  padding: 24px;
}

/* =========================================
   ローディング表示
   ========================================= */
.content {
  display: flex;

  flex-direction: column;

  align-items: center;

  gap: 16px;
}

/* =========================================
   スピナー
   ========================================= */
.spinner {
  width: 40px;
  height: 40px;

  border: 4px solid #ddd;

  /* 上側だけ色を変えることで、
     回転していることが分かるようにする */
  border-top-color: #333;

  border-radius: 50%;

  /* spinnerAnimationを繰り返し実行する */
  animation:
    spinnerAnimation 0.8s linear infinite;
}

/* =========================================
   スピナーの回転アニメーション
   ========================================= */
@keyframes spinnerAnimation {
  from {
    transform: rotate(0deg);
  }

  to {
    transform: rotate(360deg);
  }
}

/* =========================================
   メッセージ
   ========================================= */
.message {
  margin: 0;

  color: #666;

  font-size: 15px;
}

動作確認

画面を表示する際、「データを読み込んでいます」が表示されることを確認。

フェーズ5-14:共通ヘッダー・ナビゲーション

今回はNext.jsのRoute Group + nested layout.tsxを使う。
(authenticated)のように括弧で囲んだフォルダはURLに含まれないため、ファイルを移動しても / や /processesのURLは変わらない。

フォルダ構成を変更する

現在のログイン後画面を(authenticated)の中へ移動する。

src
└─ app
   │
   ├─ (authenticated)          ← 新規
   │  ├─ layout.tsx            ← 新規
   │  ├─ app-header.tsx        ← 新規
   │  ├─ app-header.module.css ← 新規
   │  │
   │  ├─ page.tsx              ← 現在の app/page.tsx を移動
   │  ├─ dashboard.module.css  ← 一緒に移動
   │  │
   │  └─ processes             ← フォルダごと移動
   │     ├─ page.tsx
   │     ├─ processes.module.css
   │     └─ [id]
   │        └─ tasks
   │           ├─ page.tsx
   │           ├─ actions.ts
   │           └─ tasks.module.css
   │
   ├─ actions
   │  └─ logout.ts             ← そのまま
   │
   ├─ login                    ← そのまま
   │
   ├─ error.tsx
   ├─ error.module.css
   ├─ loading.tsx
   ├─ loading.module.css
   ├─ layout.tsx               ← RootLayout
   └─ globals.css

共通ヘッダーを作る

src/app/(authenticated)/app-header.tsx

import Link from "next/link";

import {
  logoutAction,
} from "@/app/actions/logout";

import styles from "./app-header.module.css";

/**
 * ログイン後の画面で共通表示するヘッダー。
 *
 * トップ・工程進捗へのナビゲーションと
 * ログアウト機能をまとめる。
 */
export default function AppHeader() {
  return (
    <header className={styles.header}>

      {/* =====================================
          システム名
          ===================================== */}
      <div className={styles.inner}>

        <Link
          href="/"
          className={styles.logo}
        >
          工程進捗管理システム
        </Link>

        {/* ===================================
            ナビゲーション
            =================================== */}
        <nav className={styles.nav}>

          <Link
            href="/"
            className={styles.navLink}
          >
            ダッシュボード
          </Link>

          <Link
            href="/processes"
            className={styles.navLink}
          >
            工程進捗
          </Link>

          {/* =================================
              ログアウト
              ================================= */}
          <form action={logoutAction}>
            <button
              type="submit"
              className={styles.logoutButton}
            >
              ログアウト
            </button>
          </form>

        </nav>

      </div>

    </header>
  );
}

ヘッダー用CSS

src/app/(authenticated)/app-header.module.css

/* =========================================
   ヘッダー全体
   ========================================= */
.header {
  border-bottom: 1px solid #ddd;

  background-color: white;
}

/* =========================================
   ヘッダー内部
   ========================================= */
.inner {
  max-width: 1100px;

  margin: 0 auto;
  padding: 14px 24px;

  display: flex;
  align-items: center;
  justify-content: space-between;

  gap: 24px;
}

/* =========================================
   システム名
   ========================================= */
.logo {
  color: #222;

  font-size: 18px;
  font-weight: bold;

  text-decoration: none;

  white-space: nowrap;
}

/* =========================================
   ナビゲーション
   ========================================= */
.nav {
  display: flex;
  align-items: center;

  gap: 16px;
}

/* ナビゲーションリンク */
.navLink {
  color: #444;

  text-decoration: none;

  white-space: nowrap;
}

.navLink:hover {
  text-decoration: underline;
}

/* =========================================
   ログアウトボタン
   ========================================= */
.logoutButton {
  padding: 8px 14px;

  border: 1px solid #bbb;
  border-radius: 6px;

  background-color: white;

  cursor: pointer;
}

.logoutButton:hover {
  background-color: #f5f5f5;
}

/* =========================================
   スマートフォン表示
   ========================================= */
@media (max-width: 600px) {
  .inner {
    align-items: flex-start;
    flex-direction: column;

    gap: 12px;
  }

  .nav {
    width: 100%;

    flex-wrap: wrap;
  }
}

(authenticated)/layout.tsxを作成

src/app/(authenticated)/layout.tsx

import AppHeader from "./app-header";

/**
 * ログイン後画面の共通レイアウト。
 *
 * このフォルダ配下のページには、
 * AppHeaderが自動的に表示される。
 */
export default function AuthenticatedLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <>
      {/* 共通ヘッダー */}
      <AppHeader />

      {/* 各画面 */}
      {children}
    </>
  );
}

トップ画面からログアウトボタンを削除

src/app/(authenticated)/page.tsx
下記を削除する。

<form action={logoutAction}>
  <button>
    ログアウト
  </button>
</form>
import {
  logoutAction,
} from "./actions/logout";

動作確認

共通ヘッダーが表示されることを確認。

フェーズ5-15:レスポンシブ対応

スマートフォン表示に対応する。

ダッシュボード

src/app/(authenticated)/dashboard.module.css
の最後に追加する。

/* =========================================
   スマートフォン表示
   ========================================= */
@media (max-width: 600px) {

  /* 画面左右の余白を少し小さくする */
  .container {
    padding: 20px 16px;
  }

  /* タイトルを少し小さくする */
  .title {
    font-size: 24px;
  }

  /* サマリーカードを1列にする */
  .summaryGrid {
    grid-template-columns: 1fr;

    gap: 12px;
  }

  /* カード内部の余白を少し小さくする */
  .summaryCard {
    padding: 18px;
  }

  /* 数値を少し小さくする */
  .summaryValue {
    font-size: 28px;
  }

  /* 工程進捗ボタンを横幅いっぱいにする */
  .primaryLink {
    display: block;

    width: 100%;

    box-sizing: border-box;

    text-align: center;
  }
}

工程一覧

src/app/(authenticated)/processes/processes.module.css
の最後に追加する。

/* =========================================
   スマートフォン表示
   ========================================= */
@media (max-width: 600px) {

  .container {
    padding: 20px 16px;
  }

  .title {
    font-size: 24px;
  }

  /* 工程カードを1列表示にする */
  .grid {
    grid-template-columns: 1fr;
  }

  .card {
    padding: 16px;
  }

  .processName {
    font-size: 18px;
  }

  /* ステータス件数を縦方向にも折り返せるようにする */
  .statusList {
    gap: 8px 12px;
  }

  /* 完了率部分 */
  .progressArea {
    gap: 8px;
  }
}

作業一覧画面

src/app/(authenticated)/processes/[id]/tasks/tasks.module.css
の最後に追加する。

/* =========================================
   タブレット・スマートフォン表示
   ========================================= */
@media (max-width: 768px) {

  .container {
    padding: 20px 16px;
  }

  .title {
    font-size: 24px;
  }

  /* -----------------------------------------
     テーブル
     ----------------------------------------- */

  .tableWrapper {
    /*
      テーブルが画面幅を超えた場合、
      横スクロールできるようにする。
    */
    overflow-x: auto;

    /*
      iPhoneなどでスクロールを滑らかにする。
    */
    -webkit-overflow-scrolling: touch;
  }

  .table {
    /*
      列が無理に押し縮められないよう、
      最低幅を確保する。
    */
    min-width: 720px;
  }

  .table th,
  .table td {
    padding: 10px 12px;
  }

  /* -----------------------------------------
     ステータス変更ボタン
     ----------------------------------------- */

  .buttonGroup {
    gap: 4px;
  }

  .statusButton {
    padding: 7px 10px;

    font-size: 14px;
  }
}

ステータスボタンを折り返さない

tasks.module.cssの.buttonGroupが、

.buttonGroup {
  display: flex;
  gap: 6px;
}

になっているので、そのままでも基本的に横並び。
念のため、

.buttonGroup {
  display: flex;

  gap: 6px;

  flex-wrap: nowrap;
}

に変更する。

ログイン画面も確認

src/app/login/login.module.cssには、

.loginCard {
  width: 100%;
  max-width: 400px;
}

があるので、レスポンシブ対応できている。

ただしスマホ端でカードが画面いっぱいになり過ぎる可能性があるため、.containerに、

.container {
  min-height: 100vh;

  display: flex;
  justify-content: center;
  align-items: center;

  background-color: #f5f5f5;

  padding: 16px;
}

と padding: 16px;を追加しておくと安全。

また.loginCardに、

box-sizing: border-box;

も追加する。

動作確認

Chromeでスマホ表示を確認する。
ChromeでF12を押す。

上部にあるスマホ・タブレットのアイコン、

Toggle device toolbar

を押す。

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?