LEMP環境で、PHPとReact(TypeScript)を使って、初心者でもできる簡単な掲示板アプリの作り方

プログラミングの勉強方法として、自分で、簡単なアプリを作成しようと考えている方がいるかと思います。

しかし、どんなアプリを作れば良いか分からない方も多いかと思います。

今回は、そんな方の力になれれば良いと思い、

筆者が、作成したphpやTypeScriptを使って、簡単な掲示板アプリを作る方法を公開します。

作成する掲示板は掲示板は、データベースを利用する簡単な掲示板
  
　　使用言語　　バックエンド：PHP　　※フレームワークは使用しない
             フロントエンド: React、typescript
             
　　データベース：MySQL
　
　▼WEBシステム
　　投稿内容表示ページ
　　　Reactのsrc/App.tsx
　　　
開発環境は、Dockerを使い、LEMP環境で行う。


まずは、ターミナルで、

![スクリーンショット 2024-11-26 17.20.03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/a8773492-087c-7c29-99c3-c91ea9efa810.png)


keijibanというディレクトリを作成。
```
$mkdir keijiban
$cd keijiban
```
<font color="red">※ $マークは、無視していいです。(ターミナルで入力するという意味です。)</font>

でkeijibanディレクトリに移動

frontディレクトリにReactをインストール

# 事前準備
```
# インストール
$ brew install nodebrew
```
```
# nodebrewがインストールされたか確認
$ nodebrew -v
```
```
# npmも一緒にインストールされているか確認
$ npm -v
```
```
# セットアップ
$ nodebrew setup
```
```
# 以下のexport文を~/.zshrcもしくは~/.bash_profileに追記
$ vim ~/.zshrc
```
```
# export PATH=$HOME/.nodebrew/current/bin:$PATH
# 反映
$ source ~/.zshrc
```

# nodeのインストール

```
$ nodebrew install stable
```
```
# インストール済みのnodeバージョンの一覧を表示
$ nodebrew ls
$ nodebrew use 使用するnodeのバージョン
# ex) nodebrew use v18.8.0
```

まずは、nodeのバージョンの確認
```
$ node -v
```
yarnをインストール
```
$ npm install --global yarn
$ yarn --version
```

React.jsの場合はこちら↓
```
$ yarn create react-app frontend
```

React + TypeScriptの場合はこちら↓
```
$ yarn create react-app frontend --template typescript
```

作成したプロジェクト配下に移動し、下記のstartコマンドを実行します。
```
$ cd  frontend
$ yarn start
```

実行後、ブラウザが起動し、以下の画面が表示されていればOKです。

![新規メモ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/4f1a6a30-5204-6e0c-6314-cdf5652b2346.jpeg)







#### dockerのインストール

こちらのサイトを参考にしてみるといいです。↓

https://docs.docker.jp/desktop/install/mac-install.html

Dockerが、ちゃんとインストールできているかは、
```
$docker --version
```
で確認



# 1.フロントエンド

APIを利用するコンポーネント

#### 1-1. 入力フォームを実装するために使う関数から書いていきます。
```typescript:keijiban/frontend/src/App.tsx
import './App.css'; 
import React, { useEffect, useState } from 'react'; 
// Postインターフェースの定義 
interface Post {
post_id: number;  // 投稿のID 
author_name: string;  // 投稿者の名前 
title: string;  // 投稿のタイトル
 content: string;  // 投稿の内容 
created_at: string;  // 投稿日時
 } 
const App: React.FC = () => { 
// ステートの定義 
const [posts, setPosts] = useState<Post[]>([]); // 投稿リストの状態 
const [page, setPage] = useState<number>(1);  // 現在のページ番号 
const[message, setMessage] = useState<string>(''); // メッセージの状態 
const [newPost, setNewPost] = useState<{ author_name: string; title: string; content: string}>({ author_name: '', title: '', content: ''}); // 新しい投稿の状態

 // コンポーネントがマウントされたときにデータをフェッチするためのuseEffect 

useEffect(() => { const fetchData = async () => { try { const response = await fetch(`http://localhost:8080/index.php?page=${page}`); 
// 投稿を取得するためのリクエスト
 if (!response.ok) throw new Error('ネットワークエラー');
 // エラーチェック 
const data = awaitresponse.json(); 
// JSONデータをパース 
setPosts(data.posts || []); 
// 投稿データを設定
 setMessage(data.message); 
// サーバーからのメッセージを設定 
} catch (error) { console.error(error); 
// エラーをコンソールに表示setMessage('データの取得に失敗しました。'); 
// エラーメッセージを設定 } }; fetchData(); // データのフェッチを実行 }, [page]); // pageが変更されたときに再実行
```



#### 1-2.投稿を送信する関数(handleSubmit)を作成

```typescript:keijiban/frontend/src/App.tsx
// 投稿の送信
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const response = await fetch('http://localhost:8080/post.php', {
        method: 'POST',
        headers: {'Content-Type':  'application/x-www-form-urlencoded' },
        body: new URLSearchParams(newPost).toString(),
      });
      if (!response.ok) throw new Error('送信エラー');
      const data =await response.json();
      if (data.message) {
          setPosts(prevPosts => [
              ...prevPosts,
              {
                  post_id: Date.now(),
                  author_name:  newPost.author_name,
                  title: newPost.title,
                  content: newPost.content,
                  created_at: new Date(). toISOString(), // 現在の日時を設定
              },
          ]);
          setNewPost({ author_name: '', title: '', content: ''}); // 入力フォームをリセット
      }
      
      setMessage(data.message);
      setPage(1);
    } catch (error) {
      console.error('送信するのにエラーが発生しました。', error);
      setMessage('投稿に失敗しました。');
    }
  };
```

#### 1-3. handleSubmit関数のあとに、投稿を削除する関数(handleDelete)を作成

```typescript:keijiban/frontend/src/App.tsx
const handleDelete = async (postId: number) => {
    try {
      const response = await fetch('http://localhost:8080/delete.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ post_id: postId.toString() }).toString(),
      });
      const data = await response.json();
      setMessage(data.message); // メッセージを設定
      // 投稿一覧を更新
      setPosts(prevPosts => prevPosts.filter(post => post.post_id !== postId));
    } catch (error) {
      console.error('削除中にエラーが発生しました。', error);
      setMessage('削除に失敗しました。');
    }
```

#### 1-4. タイトルと投稿内容、「投稿する」ボタン、投稿一覧までのデザイン、ページネーションまでを作成します。
```typescript:keijiban/frontend/src/App.tsx
return (
    <div>
        <h1>掲示板</h1>
        <form onSubmit={handleSubmit}>
         <div className="form-container">
            <input type="text" value={newPost.author_name} onChange={e => setNewPost({ ...newPost, author_name: e.target.value })} placeholder="投稿者" required />
            <input type='text' value={newPost.title} onChange={e => setNewPost({ ...newPost, title: e.target.value})} placeholder="タイトル" required />
            <textarea value={newPost.content} onChange={e => setNewPost({ ...newPost, content: e.target.value})} placeholder="ここにメッセージを入力してください。" required />
            </div>
            <button type="submit" className="submit-button">投稿する</button>
        </form>
        
        {message && <div className="error">{message}</div> }
        <h2>投稿一覧</h2>
        {posts.map(post => (
            <div className="post" key={post.post_id} >
                <h3>{post.title}</h3>
                <p>{post.content}</p>
                <p>投稿者: {post.author_name} | 投稿日時: {post.created_at}</p>                
                    <input type="hidden" name="post_id" value={post.post_id} />
                    <button onClick={() => handleDelete(post.post_id)}  type="submit" className='delete-button'>削除</button>
            </div>
        ))}
        <div className="button-container">
          {page > 1 && (
            <button onClick={() => setPage(prev => Math.max(prev - 1, 1))} className="prev-button">前へ</button> 
            )}
          {page < totalPages && (
            <button onClick={() => setPage(prev => prev + 1)} className="next-button">次へ</button>
          )}
        </div>
      </div>
  );
```

これらを繋ぎ合わせると、

Reactのsrc/App.tsxの全体のコードはこちらになります
```typescript:keijiban/frontend/src/App.tsx
import './App.css';
import React, { useEffect, useState }from 'react';
interface Post {
  post_id: number;
  author_name: string;
  title: string;
  content: string;
  created_at: string;
}
const App: React.FC = () => {
  const [posts, setPosts] = useState<Post[]>([]);
  const [page, setPage] = useState<number>(1);
  const [message, setMessage] = useState<string>('');
  const [newPost, setNewPost] = useState<{ author_name: string; title: string; content: string}>({ author_name: '', title: '', content: ''});
  // 総ページ数の状態を追加
  const [totalPages, setTotalPages] = useState<number>(1);
  useEffect(() => {
  
  console.log('Current Page:', page);
  console.log('Total Pages:', totalPages);
  console.log('Show Previous:', page > 1);
  console.log('Show Next:', page < totalPages);
    
    const fetchData = async () => {
      console.log('データを送信しました。');
      try {
        const response = await fetch(`http://localhost:8080/index.php?page=${page}`);
        if (!response.ok) throw new Error('ネットワークエラー');
        const data = await response.json();
        
        setPosts(data.posts || []);
        setMessage(data.message);
        setTotalPages(data.total_pages); // 総ページ数を設定
        console.log('Total Page :', data.total_pages);
      } catch (error) {
        console.error(error);
        setMessage('データの取得に失敗しました。');
      }
    };
    fetchData();
  }, [page]);
  
  // 投稿の送信
  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    try {
      const response = await fetch('http://localhost:8080/post.php', {
        method: 'POST',
        headers: {'Content-Type':  'application/x-www-form-urlencoded' },
        body: new URLSearchParams(newPost).toString(),
      });
      if (!response.ok) throw new Error('送信エラー');
      const data =await response.json();
      if (data.message) {
          setPosts(prevPosts => [
              ...prevPosts,
              {
                  post_id: Date.now(),
                  author_name:  newPost.author_name,
                  title: newPost.title,
                  content: newPost.content,
                  created_at: new Date(). toISOString(), // 現在の日時を設定
              },
          ]);
          setNewPost({ author_name: '', title: '', content: ''}); // 入力フォームをリセット
      }
      
      setMessage(data.message);
      setPage(1);
    } catch (error) {
      console.error('送信するのにエラーが発生しました。', error);
      setMessage('投稿に失敗しました。');
    }
  };
  // 投稿の削除
  const handleDelete = async (postId: number) => {
    try {
      const response = await fetch('http://localhost:8080/delete.php', {
        method: 'POST',
        headers: {'Content-Type': 'application/x-www-form-urlencoded' },
        body: new URLSearchParams({ post_id: postId.toString() }).toString(),
      });
      const data = await response.json();
      setMessage(data.message); // メッセージを設定
      // 投稿一覧を更新
      setPosts(prevPosts => prevPosts.filter(post => post.post_id !== postId));
    } catch (error) {
      console.error('削除中にエラーが発生しました。', error);
      setMessage('削除に失敗しました。');
    }
  };
  
  return (
    <div>
        <h1>掲示板</h1>
        <form onSubmit={handleSubmit}>
         <div className="form-container">
            <input type="text" id="author_name" value={newPost.author_name} onChange={e => setNewPost({ ...newPost, author_name: e.target.value })} placeholder="投稿者" required />
            <input type='text' id="title" value={newPost.title} onChange={e => setNewPost({ ...newPost, title: e.target.value})} placeholder="タイトル" required />
            <textarea id="content" value={newPost.content} onChange={e => setNewPost({ ...newPost, content: e.target.value})} placeholder="ここにメッセージを入力してください。" required />
            </div>
            <button type="submit" className="submit-button">投稿する</button>
        </form>
        
        {message && <div className="error">{message}</div> }
        <h2>投稿一覧</h2>
        {posts.map(post => (
            <div className="post" key={post.post_id} >
                <h3>{post.title}</h3>
                <p>{post.content}</p>
                <p>投稿者: {post.author_name} | 投稿日時: {post.created_at}</p>                
                    <input type="hidden" name="post_id" value={post.post_id} />
                    <button onClick={() => handleDelete(post.post_id)}  type="submit" className='delete-button'>削除</button>
            </div>
        ))}
        <div className="button-container">         
        {page > 1 && (
            <button onClick={() => setPage(prev => Math.max(prev - 1, 1))
              
            } className="prev-button">前へ</button> 
            )}
          {/*ページ番号の表示*/}
          {Array.from({ length: totalPages}, (_, index) => (
            <button
            key={index + 1}
            onClick={() => setPage(index + 1)}
            className={`page-button ${page === index + 1 ? 'active-page' : ''}`}
            >
              {index + 1}
            </button>
          ))}
          
        {page < totalPages && (
            <button onClick={() => setPage(prev => prev + 1)
            } className="next-button">次へ</button>
        )}          
        </div>
      </div>
  );
};
export default App;
``` 
  
このままでは、デザインが無茶苦茶な状態なので、

frontendディレクトリ内にある
srcディレクトリ内のApp.cssを書き換えて、

デザインを整えていきます。

#### デザインの整理 

```css:keijiban/frontend/src/App.css
form-container {
  align-items: center;
  display: flex;
  flex-direction: column; /* 縦に並べるための設定 */
  margin-bottom: 20px;
}
h1, h2, h3 {
  text-align: center;
}
textarea {
  margin:  10px 0; /* 上下に余白を追加 */
  width: 100%;
  height: 80px;
  padding: 8px;
  border: 1px solid #ccc; /* ボーダーの設定 */
  border-radius:  4px; /* 角を丸める */
}
input {
  margin:  10px 0; /* 上下に余白を追加 */
  width: 50%;
  padding: 8px;
  border: 1px solid #ccc; /* ボーダーの設定 */
  border-radius:  4px; /* 角を丸める */
}
.post {
  max-width: 600px;  /* 投稿の最大幅を設定 */
  margin:  20px auto; /* 上下の余白を20px、左右の余白を自動にして中央揃え */
  padding:  20px; /* 内側の余白 */
  border: 1px solid #ccc; /* 境界線 */
  border-radius:  8px; /* 角を丸める */
  background-color:  #f9f9f9; /* 背景色 */
  box-shadow: 0 2px 4px rgba(0,0,0, 0.1) /* 影を追加 */
}
form {
  display: flex;
  flex-direction: column; /* 縦に並べる */
  align-items: center; /* 中央揃え */
}
button.submit-button {
  text-align:center;
  width: 100px;
  background-color: green;
  font-weight: bold;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
  margin-top: 20px;
}
button.submit-button:hover {
  background: darkgreen;
}
.button-container {
  display: flex;
  justify-content: center;
  margin-top: 20px;
}
button.prev-button {
  width: 80px;
  font-weight: bold;
  background-color: gray;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
}
button.prev-button:hover {
  background-color: darkgray;
}
button.next-button {
  width: 80px;
  font-weight: bold;
  background-color: blue;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
}
button.next-button:hover {
  background-color: darkblue;
}
button.delete-button {
  display: block; /* ボタンをブロック要素に */
  margin: 0 auto; /* 自動余白で中央揃えで */
  width: 80px;
  font-weight: bold;
  background-color: red;
  color: white;
  border: none;
  padding: 10px;
  border-radius: 4px;
  cursor: pointer;
}
button.delete-button:hover {
  background-color: darkred;
}
.submit-button, .prev-button, .next-button, .delete-button {
  align-items: center;
  margin: 0 20px; /* ボタン同士の間隔を調整 */
}
.error {
  margin-top: 20px;
  text-align: center;
  color: red;
}
.active-page {
  font-weight: bold;
  color: blue;
  border-bottom: 2px solid blue;
}
.page-button {
  font-size: 18px;
  background-color: transparent; /* 背景を透明にする */
  border: none; /* ボーダーをなくす */
  color: blue;
  cursor: pointer;
  margin: 0 15px; /* 左右に10pxの余白を追加 */
}
.page-button:hover {
  text-decoration: underline;
}
```

すると、図のようなデザインになります。

![掲示板.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/32a044da-3456-9765-1bd0-a988f9b14a20.jpeg)

# 2.Docker環境の準備

Dockerを起動させただけで、バックエンドであるPHPとフロントエンドであるReact両方を立ち上げるようにします。

DockerコンテナのIPアドレスの調べ方

1.コンテナのIDまたは名前を取得します。
```
$ docker ps
```

2. 特定のコンテナの詳細を表示
```
$ docker inspect <コンテナIDまたは名前>
```
3. 出力の中から、"Networks"セクションを探し、"IPAddress"の値を確認する。


# ディレクトリの構造
```
keijiban/
├── docker-compose.yml
├── Dockerfile
├── nginx/
│   └── default.conf
├── php/
│   ├── index.php
│   ├── post.php
│   └── delete.php
├── mysql/
│   └── init.sql
└── frontend/
    ├──(Reactアプリケーション)
    ├──Dockefile
```

#### 2-1.docker-compose.ymlの作成

```yml:keijiban/docker-compose.yml
version : '3.8'
services:
  nginx:
    image: nginx:alpine
    ports:
     - "8080:80"
    volumes:
     - ./nginx/default.conf:/etc/nginx/conf.d/default.conf
     - ./php:/var/www/html
    depends_on:
     - php
  php:
    build:
      context: .
      dockerfile: Dockerfile
    volumes:
      - ./php:/var/www/html
    depends_on:
      - mysql
  mysql:
    image: mysql:5.7
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: keijiban
      MYSQL_USER: user
      MYSQL_PASSWORD: password
    volumes:
      - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
    ports:
      - "3306:3306"
  frontend:
    build:
      context: ./frontend
    ports:
      - "3000:80"
```

#### 2-2.Dockerfileの作成
```Dockerfile:keijiban/Dockerfile
FROM php:8.3-fpm
# PHP拡張をインストール
RUN docker-php-ext-install mysqli pdo pdo_mysql
```
frontend ディレクトリ内にも Dockerfile を作成

frontend/Dockerfile
```Dockerfile:keijiban/frontend/Dockerfile
# Node.jsの公式イメージを使用
FROM node:16
# アプリケーションの作業ディレクトリを作成
WORKDIR /app
# 依存関係をインストールするためにpackage.jsonをコピー
COPY package*.json ./
# 依存関係をインストール
RUN npm install
# 残りのアプリケーションコードをコピー
COPY . .
# アプリケーションをビルド
RUN npm run build
# ビルドされたアプリケーションを提供するためにnginxを使用
FROM nginx:alpine
COPY --from=0 /app/build /usr/share/nginx/html
# ポート80を開放
EXPOSE 80
# Nginxを起動
CMD ["nginx", "-g", "daemon off;"]
```


#### 2-3.Nginx設定ファイル
```conf:keijiban/nginx/default.conf
server {
    listen 80;
    server_name localhost;
    root /var/www/html;
    index index.php index.html index.htm;
    location / {
        # CORSの設定
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'Content-Type' always;
        # OPTIONSメソッドの処理
        if ($request_method = 'OPTIONS') {
            add_header 'Content-Length' 0;
            return 204; #Contentがない場合
        }
        try_files $uri $uri/ /index.php?$query_string;
    }
    location ~ \.php$ {
        include fastcgi_params;
        fastcgi_pass php:9000;
        fastcgi_index index.php;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    }
    location ~ /\.ht {
        deny all;
    }
}
```

#### 2-4.掲示板アプリケーションで必要なテーブルを作成するためのSQLコードを記述(MySQL初期化スクリプト)


```sql:keijiban/mysql/init.sql
-- データベースを使用
CREATE DATABASE IF NOT EXISTS keijiban;
USE keijiban;

-- 投稿内容テーブルの作成
CREATE TABLE IF NOT EXISTS posts (
    post_id INT AUTO_INCREMENT PRIMARY KEY,
    author_name VARCHAR(50) NOT NULL,
    title VARCHAR(100) NOT NULL,
    content TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 初期データの挿入
INSERT INTO posts (author_name, title, content) VALUES
('テストユーザー', '初投稿', 'これはテストメッセージです。'),
('ユーザー1', '投稿1', 'これは投稿1の内容です。'),
('ユーザー2', '投稿2', 'これは投稿2の内容です。');
```

# 3.バックエンド

#### 3-1.データベースと連携し、 Reactにページネーションを反映させるための処理

```php:keijiban/php/index.php
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
$mysqli = new mysqli("mysql", "user", "password", "keijiban");
if ($mysqli->connect_error) {
    die("Connection failed: " . $mysqli->connect_error);
}
// ページネーションの作成
$limit = 20;
$page = isset($_GET['page']) ? (int)$_GET['page'] : 1;
$offset = ($page - 1) * $limit;
// 投稿の取得
$result = $mysqli->query("SELECT * FROM posts ORDER BY created_at DESC LIMIT $limit OFFSET $offset");
if (!$result) {
    die("クエリエラー: " . $mysqli->error);
}
$total_posts_result = $mysqli->query("SELECT COUNT(*) FROM posts");
$total_posts = $total_posts_result->fetch_row()[0];
$total_pages = ceil($total_posts / $limit);
// 取得したデータを配列に格納
$posts = [];
while ($row = $result->fetch_assoc()) {
    // post_idを整数に変換
    $row['post_id'] = (int)$row['post_id'];
    $posts[] = $row;
}
// JSON形式でレスポンスを返す
echo json_encode([
    'posts' => $posts,
    'message' => isset($_GET['message']) ? htmlspecialchars($_GET['message']) : '',
    'total_pages' => max(1, $total_pages),// 総ページを追加
])
?>
```
#### 3-2. データベースに投稿者やタイトル、コメントや内容などを送り、その内容をReactに反映するための処理

```php:keijiban/php/post.php
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
// MySQL接続
$mysqli = new mysqli("mysql", "user", "password", "keijiban");
if ($mysqli->connect_error) {
    die("Connection failed: " . $mysqli->connect_error);
}
// OPTIONSリクエストの処理
if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') {
    header("HTTP/1.1 204 No Content");
    exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    header('Content-Type: application/json');
    //入力値の取得
    $author_name = $_POST['author_name'] ?? '';
    $title = $_POST['title'] ?? '';
    $content = $_POST['content'] ?? '';
    // ブレースホルダーを使ったクエリの準備
    $stmt = $mysqli->prepare("INSERT INTO posts (author_name, title, content) VALUES (?, ?, ?)");
    if (!$stmt) {
      echo json_encode(['message' => '処理できませんでした。: ' . $mysqli->error]);
      exit;
    }
    $stmt->bind_param("sss", $author_name, $title, $content);
    if($stmt->execute()){
      echo json_encode(['message' => 'メッセージを送信しました。']);  
    } else {
      echo json_encode(['message' => 'メッセージが送れませんでした。', 'error' => $stmt->error]);
    }
    
    $stmt->close();
}
$mysqli->close();
?>
```

#### 3-3.データベース内に残った投稿内容や Reactで表示されている投稿内容の削除を反映するための処理

```php:keijiban/php/delete.php
<?php
header("Access-Control-Allow-Origin: *");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Content-Type");
header("Content-Type: application/json");
$mysqli = new mysqli("mysql", "user", "password", "keijiban");
if ($mysqli->connect_error) {
    echo json_encode(["error" => "Connection failed: " . $mysqli->connect_error]);
    exit;
}
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $post_id = isset($_POST['post_id']) ? (int)$_POST['post_id'] : 0;
    
    if ($post_id > 0) {
        if ($mysqli->query("DELETE FROM posts WHERE post_id = $post_id")) {
            echo json_encode(["message"=> "削除成功"]);
        } else {
            echo json_encode(["error" => "削除失敗"]);
        }
    } else {
        echo json_encode(["error" => "無効なID"]);
    }
} else {
    echo json_encode(["error" => "無効なリクエスト"]);
}
$mysqli->close();
?>
```


# 4.Dockerの起動
```
$ docker-compose up -d –build
```
下の画像のようなメッセージが出れば成功です。

![transferring.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/8f94f30c-f195-4c7f-b82e-21552867f430.jpeg)


#### 4-1. PHPファイルの存在確認

以下のコマンドを実行して、Docker内にコンテナ内に post.php が存在することを確認します。
```
$ docker-compose exec php ls /var/www/html
```

#### 4-2. MySQLに接続できているか確認するコマンド

```
$ docker exec -it keijiban-mysql-1 mysql -u root -p
```

パスワード名：password

1. データベースを選択
接続後、以下のコマンドでデータベースを選択します。
```
USE keijiban;
```
2. テーブルの状態を確認
次に、テーブルの一覧を表示するために以下のコマンドを実行します。
```
SHOW TABLES;
```
特定のテーブルの内容を確認したい場合は、次のようにします。
```
SELECT * FROM posts;
```
このような内容になっていたら、大丈夫です。

![AD_4nXe04Qs-PEhat5C6gqI2iG7daTNdD3rZbw6jQvQe6Ip-ZDoecp9mNtKT5GQW_wa3tzOWZZj3xgE9fSMQ2YD30-ivTNj54eNMN-iOeaod6HiOOlqjVqSsXECF.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/1231628d-3072-7fb1-df43-723cb47354f8.jpeg)

# 5. アプリを起動 

書いたコードをテストをする際は、

ブラウザで、http://localhost:3000と入力してください。

掲示板のフォームに以下の内容で入力

![AD_4nXevP3vAB_CNdQg1t7QnHQN_1n9t63aXVMqIFT-V4KJz9xCgqN3szpetgYy6lcodYXke2wSnxfIkq1OyVg9SjIVNUF3QRnO26qH3EzI-kh5zipXkFpVuj3bH.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/2392613c-c3a4-5b4d-d3bb-ceb35b765591.jpeg)


「投稿する」ボタンを押すと、

![AD_4nXc6xPM5LEStRdpe0yQTzVuQLWd_PAHyzrXKSd2KkUMUW90gNlAjFjJ5MN7ORgSsnNfH_sj7heBBG-6HsiQFsU6JV_H6bbwJhQj5PJEYZJ72cVZkXCzjUwop.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/7b8e7f15-c646-5803-6676-7158a69445c8.jpeg)


ちゃんと反映されました。

連続で投稿しても投稿一覧に履歴が残っていってます。

![AD_4nXegg16iyxAybXNpsio6Qv2UwegKXJ5Bhvxmq_Ts2CuWelIbuQMZuwmHUifcnIuOw-3iLVALzGIwyvcOnnAjkACX_MUOqx0uF_l_jBFTHIvEnL83-MIN0qul.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/f88dff59-a6b8-210e-9a4c-6b7075c52845.jpeg)




これらの画面が表示されれば、ちゃんと実装できています。

そして、20件以上投稿すると、

![ここにメッセージを入力してください。.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/377fd990-2bab-8db5-5c49-f811a8e80bc3.jpeg)

![AD_4nXcrBX2nCeAQs7mdvpabYJKz3IrU5EeD-8yFE5DBePLP2KQB12FeTUUrESp97K9UISP2Sr8ZMdhMTLDHpA-T_tW4oXGFx16M1Eagrqdx2kyWt9PgA7Fl3QB0.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/42f8614b-e6c3-444d-55d1-ef04bc5ad1e9.jpeg)

![AD_4nXcnz81iDRnXiOyYnpYkM6H8YyeV42eYrc2WmBRdBx7KWR-RbDNhmrtbIBGle3WQR-gVR5bsik-LvIps1RXDPSVWEmSFXAQp7CSIk9kqB5O5kSxoPuGW_Kvv.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/ecf4dbc9-39f8-3210-4f83-1b9f52d74da8.jpeg)

ページネーションもちゃんと表示されています。


もし、20件以上投稿しても、ページネーションが表示されず、投稿が増えていく場合は、


サイトを出て、再度、http://localhost:3000と入力すると、

ページネーションがちゃんと反映されます。


#### ※ ページネーションの反映には、15分ほどかかる場合があります。


データベースを利用した簡単な掲示板アプリは、これで完成です。
では、これをgithubに上げていきましょう。

エンジニア業界に就職するには、自分がどんなものが作れるか、どれくらいコードが書けるかなどスキルを証明する必要があります。


githubは、就職する際に、それらを示すための必要なツールです。


githubに上げるやり方は、こちらのサイトを参考にすると分かりやすいと思います。↓

https://qiita.com/shungo_m/items/b73ff5b1ec6d69bb5c50

下のような画面が出てきたら、成功です。

![export.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/5fb6fb41-5aad-acd5-2367-cfa94b6cf6c0.jpeg)



ここまでの作業内容や掲示板アプリのディレクトリ構造が、この下のURLに記録されます。↓

https://github.com/makkyo-git/keijiban


今回は、フロントエンドでは、ReactというTypeScriptのフレームワーク、

バックエンドは、PHP(フレームワークなし)で作成しましたが、

次は、フロントエンドでNext.jsというTypeScriptねフレームワーク、

バックエンドでは、 LaravelというPHPのフレームワークを使って、

簡単な勤怠管理アプリを作成していきたいと思います。↓

https://qiita.com/makkyo-git/items/3ed42fc483620c16fce8