はじめに
こんにちは、咲夜です。
今回は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記事の一覧
5.2記事の詳細
5.3管理画面(記事一欄)
5.4管理画面(記事の作成)
5.5認証画面
6. ER図
7. OpenAPI仕様書
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