【企画から開発までを実践】Next.jsでwebアプリを作ってみよう #1

# はじめに
こんにちは、咲夜です。
今回はWebアプリをゼロから開発していく過程を記事にしていきたいと思います。
記事に残しながら進めていくのは初めてなのでどうなるかわからないですが、暖かい目で見守っていただければ幸いです。


# 企画
今回は私自身が普段考えているエンジニア関連以外のことをアウトプットするオリジナルの場所が欲しかったのでBlogサイトを作りたいと思います。

イメージとしては
①管理者ページから管理者が記事の作成、編集、削除、公開設定を操作する。
②記事一覧ページからユーザーは記事の詳細を確認できる。


# 要件定義・設計

## 1. システム概要
### 1.1 目的
- 個人のアウトプットを目的としたブログサイトの構築
- 管理者による記事の管理と一般ユーザーによる閲覧機能の提供

### 1.2 システム構成
#### フロントエンド
- React / Next.js
- TypeScript
- TailwindCSS
- shadcn/ui

#### バックエンド
- Node.js
- Hono
- Prisma
- Supabase (PostgreSQL)

#### インフラ
- Vercel (ホスティング)

## 2. 機能要件
### 2.1 ユーザー認証機能
#### 管理者ログイン
- 管理者専用のログインページ
- メールアドレスとパスワードによる認証

### 2.2 記事管理機能
#### 記事作成
- タイトル入力
- 本文入力（リッチテキストエディタ）
- タグ設定
- 公開/非公開設定

#### 記事編集
- 既存記事の編集
- 変更履歴の保存

#### 記事削除
- 記事の完全削除
- 削除前の確認ダイアログ

#### 公開設定
- 公開/非公開の切り替え
- 記事のステータス管理（公開済み/非公開）

### 2.3 記事閲覧機能
#### 記事一覧表示
- 公開済み記事の一覧表示
- ページネーション
- 検索機能

#### 記事詳細表示
- 記事本文の表示

## 3. データベース設計
### 3.1 テーブル構成
#### usersテーブル
- id (UUID): プライマリーキー
- email (String): メールアドレス、unique
- password (String): ハッシュ化されたパスワード
- created_at (DateTime): 作成日時
- updated_at (DateTime): 更新日時

#### articlesテーブル
- id (UUID): プライマリーキー
- title (String): 記事タイトル
- content (Text): 記事本文
- status (Boolean): 公開ステータス
- author_id (UUID): 作成者ID（外部キー）
- created_at (DateTime): 作成日時
- updated_at (DateTime): 更新日時

## 4. 非機能要件
### 4.1 セキュリティ要件
- SSL/TLS通信の必須化
- クロスサイトスクリプティング対策
- SQLインジェクション対策
- CSRF対策



## 5. UI
### 5.1記事の一覧
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/c4bd55d8-c0f0-eb6a-3412-cd5d55b8c800.png)

### 5.2記事の詳細
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/a5c7a328-9777-3d8f-0544-b800fe489f92.png)

### 5.3管理画面(記事一欄)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/f876ea71-edfe-4621-99f9-2aa4c0f40b6f.png)

### 5.4管理画面(記事の作成)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/7fec846a-85b6-fc5b-c754-223d6836ca2f.png)

### 5.5認証画面
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/53cfb9ee-fd1d-0f01-a8fb-b2580b940b8f.png)



## 6. ER図
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3942257/c2d377b7-befe-a39b-1e7b-757b262f1bad.png)

## 7. OpenAPI仕様書


```yaml
openapi: 3.0.0
info:
  title: ブログ管理システムAPI
  version: 1.0.0
  description: 個人ブログの管理システムのためのAPI仕様

servers:
  - url: https://api.blog.example.com/v1
    description: 本番環境
  - url: https://api-staging.blog.example.com/v1
    description: ステージング環境

paths:
  /auth/login:
    post:
      summary: 管理者ログイン
      tags:
        - 認証
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              properties:
                email:
                  type: string
                  format: email
                password:
                  type: string
                  format: password
      responses:
        '200':
          description: ログイン成功
          content:
            application/json:
              schema:
                type: object
                properties:
                  token:
                    type: string
                  user:
                    $ref: '#/components/schemas/User'
        '401':
          description: 認証エラー

  /articles:
    get:
      summary: 記事一覧の取得
      tags:
        - 記事
      parameters:
        - name: page
          in: query
          schema:
            type: integer
            default: 1
        - name: limit
          in: query
          schema:
            type: integer
            default: 10
        - name: status
          in: query
          schema:
            type: string
            enum: [published, draft]
      responses:
        '200':
          description: 記事一覧
          content:
            application/json:
              schema:
                type: object
                properties:
                  articles:
                    type: array
                    items:
                      $ref: '#/components/schemas/Article'
                  pagination:
                    $ref: '#/components/schemas/Pagination'

    post:
      summary: 新規記事作成
      tags:
        - 記事
      security:
        - BearerAuth: []
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArticleInput'
      responses:
        '201':
          description: 記事作成成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'

  /articles/{articleId}:
    get:
      summary: 記事詳細の取得
      tags:
        - 記事
      parameters:
        - name: articleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '200':
          description: 記事詳細
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'

    put:
      summary: 記事の更新
      tags:
        - 記事
      security:
        - BearerAuth: []
      parameters:
        - name: articleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ArticleInput'
      responses:
        '200':
          description: 記事更新成功
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Article'

    delete:
      summary: 記事の削除
      tags:
        - 記事
      security:
        - BearerAuth: []
      parameters:
        - name: articleId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      responses:
        '204':
          description: 記事削除成功

components:
  schemas:
    User:
      type: object
      properties:
        id:
          type: string
          format: uuid
        email:
          type: string
          format: email
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    Article:
      type: object
      properties:
        id:
          type: string
          format: uuid
        title:
          type: string
        content:
          type: string
        status:
          type: boolean
        author_id:
          type: string
          format: uuid
        created_at:
          type: string
          format: date-time
        updated_at:
          type: string
          format: date-time

    ArticleInput:
      type: object
      required:
        - title
        - content
        - status
      properties:
        title:
          type: string
        content:
          type: string
        status:
          type: boolean

    Pagination:
      type: object
      required:
        - current_page
        - total_pages
        - total_items
        - per_page
        - has_next
        - has_previous
      properties:
        current_page:
          type: integer
          description: 現在のページ番号
          example: 1
        total_pages:
          type: integer
          description: 総ページ数
          example: 10
        total_items:
          type: integer
          description: 総アイテム数
          example: 100
        per_page:
          type: integer
          description: 1ページあたりの表示件数
          example: 10
        has_next:
          type: boolean
          description: 次のページが存在するか
          example: true
        has_previous:
          type: boolean
          description: 前のページが存在するか
          example: false
```