LEMP環境でNext.js(TypeScript)とLaravel(PHP)を利用して、勤怠管理アプリを作成してみる。〜退会ページ編〜

前回は、マイページ、作業日報ページを含めた勤怠管理ページの作成を行ないました。

次は、新規登録ページやログインページで登録した情報がデータベースに登録されているので、その情報を削除する退会ページの作成を行なっていきます。

その前に、ヘッダーメニューとして、グローバルナビゲーションの作成します。

srcディレクトリ内のcomponentsディレクトリ内にGlobalnavディレクトリを作成し、index.tsxファイルを作成します。

```tsx:frontend/src/components/Globalnav/index.tsx
import React from 'react';
import { Link } from 'react-router-dom';
import { Me } from './types';

type Props = {
  user: Me | null;
};

const GlobalNav: React.FC<Props> = ({user}) => {
    return (      
        <nav style={styles.nav}>
        { !user ? (
          // ユーザーが未ログインの場合、新規登録ボタンを表示
          <Link to="/register" style={styles.register}>新規登録</Link>
        ) : (
          // ログイン状態の場合、マイページと退会するボタンを表示
          <>
          <Link to="/leave" style={styles.link} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>退会する</Link>
         <Link to="/mypage" style={styles.iconLink}>
          { user.icon ? (<img src={user.icon} alt="マイページ" style={styles.icon}/>
        ):(
        <img src={`${process.env.PUBLIC_URL}/images/icon/user.png`} alt="マイページ" style={styles.icon} />
        )}
         </Link>
         </>
        )}
         </nav>
    );
};

const handleMouseEnter = (e:React.MouseEvent<HTMLAnchorElement>) => {
  (e.currentTarget.style as CSSStyleDeclaration).backgroundColor = styles.linkHover.backgroundColor || '';
};

const handleMouseLeave = (e:React.MouseEvent<HTMLAnchorElement>) => {
  (e.currentTarget.style as CSSStyleDeclaration).backgroundColor = styles.link.backgroundColor || '';
}


const styles: {[key: string]: React.CSSProperties} = {
  nav: {
    display: 'flex',
    position: 'fixed',
    top: 0, // 画面上部に配置
    left: 0, // 左端に配置
    width: '100vw', // ビューポートの横幅を指定
    justifyContent : 'flex-end', // コンテンツを右寄せ
    alignItems:  'center',
    padding:  '10px, 20px', // 横の余白を少し増やす
    backgroundColor: '#3d3d3d',// 暗い灰色
    boxShadow: '0 2px 4px rgba(0,0,0,0.1)',
    zIndex: 1000, // 他の要素の上に表示されるように表示
  },

  register: {
    display: 'flex',
    justifyContent: 'center', // 水平方向に中央揃え
    alignItems: 'center',
    textDecoration: 'none',
    width: '8%', 
    color: 'white',
    margin: '0 50px',
    fontSize: '18px',
    padding: '10px 10px',
    borderRadius: '6px',
    backgroundColor: 'green', // 緑色
    transition: 'background-color 0.3s',
  },

  registerHover: {
    backgroundColor: 'darkgreen', // ホバー時の色
  },


  link: {
    textDecoration: 'none',
    color: 'white',
    margin: '0 15px',
    fontSize: '16px',
    padding: '8px 12px',
    borderRadius: '4px',
    backgroundColor: '#ff69b4', // ピンク色
    transition: 'background-color 0.3s'
  },

  linkHover: {
    backgroundColor: '#e91e63', // ホバー時の色
  },

  iconLink: {
    marginRight: 'auto',
  },

  icon: {
    width: '40px',
    height: '40px',
  }
};

export default GlobalNav;
```
ログインできていない状態だと、下のような画像↓

![新規メモ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/d9ae96bd-34ce-0488-320c-97496ebd3675.jpeg)

そして、ログインできたら、「退会する」ボタンや「マイページ」へ繋がるように

アイコンのボタンを配置するように変わるようなデザインにします。

# 1.退会ページの作成

最後に追加する機能として、退会ページを作成します。

そこでは、新規登録ページで登録したユーザー情報を削除するための処理を行い、再度、新規登録ページでユーザー登録をしないと、ログインできないようにしたいと思います。

なので、srcディレクトリ内のcomponentsディレクトリ内にLeaveディレクトリを作成し、index.tsxファイルを作成します。
(src/components/Leave/index.tsx)
```tsx:frontend/components/pages/Leave/index.tsx
import React, { useState, FormEvent } from 'react';
import { useRouter } from 'next/router';

const Leave: React.FC = () => {
    const [selectedReason, setSelectedReason] = useState<string>('');
    const [otherReason, setOtherReason] = useState<string>('');
    const [errorOtherReason, setErrorOtherReason] = useState<string | null>(null);
    const router = useRouter();
   
    const onSubmit = async (event: FormEvent<HTMLFormElement>) => {
        event.preventDefault();

        if (selectedReason === 'other' &&  !otherReason.trim()) {
            setErrorOtherReason('理由を入力してください。');
            return;
        } else {
            setErrorOtherReason(null);
        }

        // POSTリクエストで退会理由を送信
        try {
        const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leave`, {
            method: 'POST',
            headers: {
                'Content-Type' : 'application/json',
            },
            body: JSON.stringify({
                userId: 'user_id',
                selectedReason,
                otherReason:  selectedReason === 'other' ? otherReason : '',
            }),
        });

        if (response.ok) {
            const result = await response.json();
            console.log(result);
            // 退会完了ページにリダイレクト
            router.push('/leave/confirm');
        } else {
            console.error('退会手続きに失敗しました。')
        }
    } catch (error) {
        console.error('エラーが発生しました:', error);
    }
 };
    return (
        <div>
            <h1>退会理由</h1>
            <form onSubmit={onSubmit}>
                <label>
                    <input
                    type="radio"
                    value="no_fun"
                    checked={selectedReason === 'no_fun'}
                    onChange={() => setSelectedReason('no_fun')}/>
                    仕事がつまらないから
                </label>
                <label>
                    <input 
                    type="radio"
                    value="change_job"
                    checked={selectedReason === 'change_job'}
                    onChange={() => setSelectedReason('change_job')}/>
                   転職が決まったから
                </label>
                <label>
                    <input
                    type="radio"
                    value="no_job"
                    checked={selectedReason === 'no_job'}
                    onChange={() => setSelectedReason('no_job')}/>
                    入れる日数が少ないから
                </label>
                <label>
                    <input
                    type="radio"
                    value="relationship"
                    checked={selectedReason === 'relationship'}
                    onChange={() => setSelectedReason('relationship')}/>
                    人間関係が悪いから
                </label>
                <label>
                    <input
                    type="radio"
                    value="other"
                    checked={selectedReason === 'other'}
                    onChange={() => setSelectedReason('other')}/>
                    その他
                </label>
                {selectedReason === 'other' && (
                    <div>
                        <textarea
                        placeholder="その理由を入力してください。"
                        value={otherReason}
                        onChange= {(e) => setOtherReason(e.target.value)}
                        onBlur={() => {
                            if (!otherReason.trim()) {
                                setErrorOtherReason('理由を入力してください。');
                            }
                        }}
                        />
                        {errorOtherReason && <p style={{color: 'red'}}>{errorOtherReason}</p>}
                    </div>
                )}
                
                <button type="submit">確認画面へ</button>
                <div className="cancel">
                <a href="/login">キャンセル</a>        
                </div>       
            </form>
            </div>
    );
};

export default Leave;
```
また、デザインを整えていきます。

Leaveディレクトリ内に、index.cssファイルを作成します。

```css:frontend/components/Leave/index.css
/* 全体のスタイル */
body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f9f9f9;
}

/* タイトルのスタイル */
h1 {
  margin-bottom: 30px;
  text-align: center;
  margin-top: 50px;
  font-size: 28px;
  font-weight: bold;
}

/* フォームの全体設定 */
form {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 600px;
  margin: 0 auto;
  padding: 20px;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}

/* ラジオボタンとテキストの整列 */
label {
  display: flex;
  align-items: center;
  font-size: 16px;
  margin-bottom: 15px;
  width: 100%;
}

/* ラジオボタンの間隔 */
label input[type="radio"] {
  margin-right: 10px;
}

/* テキストエリアのスタイル */
textarea {
  margin: 15px 0;
  width: 500px;
  height: 150px;
  padding: 10px;
  border: 1px solid #ccc;
  border-radius: 6px;
  box-sizing: border-box;
  font-size: 14px;
}

/* エラーメッセージのスタイル */
p {
  color: red;
  font-size: 14px;
  margin: 5px 0;
}

/* ボタンのスタイル */
button {
  padding: 10px 20px;
  border: none;
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  background-color: rgb(53, 133, 246);
  border-radius: 6px;
  transition: background-color 0.3s ease;
}

button:hover {
  background-color: blue;
}

/* キャンセルリンク */
.cancel a {
  display: inline-block;
  margin-top: 15px;
  color: blue;
  text-decoration: none;
  font-size: 14px;
}

.cancel a:hover {
  color: darkblue;
}
```
退会画面の最初の画面は、下のようになります。

![新規メモ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/8114ef31-cd02-7fdb-ca58-8e3ca87570b6.jpeg)

## バックエンド
Laravelでは以下の手順で実装します。

## ルートの設定
routes/api.phpに以下を追加します。

```php:laravel/routes/api.php

use App\Http\Controllers\LeaveController;

Route::post('/leave', [LeaveController::class, 'store']);
```

## コントローラーの作成
```
$ php artisan make:controller LeaveController
```
を実行してapp/Http/Controllersディレクトリ内に、LeaveController.phpファイルが作成されます。

そして、以下の内容を記述します。
```php:laravel/app/Http/Controllers/LeaveController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;

class LeaveController extends Controller
{
    public function store(Request $request)
    {
        // 入力データのバリデーション
        $validatedData = $request->validate([
            'userId' => 'required|integer',
            'selectedReason' => 'required|string',
            'otherReason' => 'nullable|string',
        ]);

        $userId = $validatedData['userId'];
        $selectedReason = $validatedData['selectedReason'];
        $otherReason = $validatedData['otherReason'] ?? null;

        // 理由の値をマッピング
        $reasonMapping = [
            'no_fun' => '仕事がつまらないから',
            'change_job' => '転職が決まったから',
            'no_job' => '入れる日数が少ないから',
            'relationship' => '人間関係が悪いから',
            'other' => 'その他',
        ];

        // 日本語の理由に変換
        $reasonText = $reasonMapping[$selectedReason] ?? '不明な理由';

        try {
            // 退会理由を保存
            DB::table('leave_reasons')->insert([
                'user_id' => $userId,
                'reason' => $reasonText, // 日本語の理由を保存
                'other_reason' => $selectedReason === 'other' ? $otherReason : null,
                'created_at' => now(),
            ]);

            return response()->json([
                'status' => 'success',
                'message' => '退会理由が正常に登録されました。',
            ], 200);
        } catch (\Exception $e) {
            return response()->json([
                'status' => 'error',
                'message' => '処理中にエラーが発生しました: ' . $e->getMessage(),
            ], 500);
        }
    }
}

```
## マイグレーションの作成
```
$ php artisan make:migration create_leave_reasons_table
```
を実行すると、
databse/migrationsディレクトリ内にXXXX_XX_XX_XXXXX_create_leave_reasons_table.phpといったファイルが作成されます。

そこに、以下のコードを追加します。
```diff_php:laravel/database/migrations/XXXX_XX_XX_XXXXX_create_leave_reasons_table.php
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    /**
     * Run the migrations.
     *
     * @return void
     */
    public function up()
    {
        Schema::create('leave_reasons', function (Blueprint $table) {
            $table->id(); // 自動インクリメントの主キー
+ $table->unsignedBigInteger('user_id'); // ユーザーID
+ $table->string('reason', 255); // 退会理由（日本語文字列を保存）
+ $table->text('other_reason')->nullable(); // その他の理由（任意）
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade'); // users テーブルの該当ユーザーが削除された場合、関連する退会理由も自動的に削除
            $table->timestamps(); // created_at と updated_at カラム
        });
    }

    /**
     * Reverse the migrations.
     *
     * @return void
     */
    public function down()
    {
        Schema::dropIfExists('leave_reasons');
    }
}
```
## マイグレーションを適用

マイグレーションファイルを作成・修正後、以下のコマンドを実行してデータベースに反映します。

```
$ php artisan migrate
```
### 補足

マイグレーションファイルに修正が必要な場合、最初に適用済みのマイグレーションをロールバックしてから再度適用します。

```
$ php artisan migrate:rollback
$ php artisan migrate
```
# 2.退会理由確認画面ページ

Leaveディレクトリ内にConfirm.tsxファイルを作成します。

```tsx:frontend/src/components/Leave/Confirm.tsx
import { useRouter } from 'next/router';
import { useMutation, useQueryClient } from 'react-query';

const LeaveConfirm: React.FC = () => {
    const router = useRouter();
    const queryClient = useQueryClient();
    // Routerから渡されるクエリパラメーターやstateの取得
    const { 
        selectedReason = '', 
        otherReason = '', 
        userId = '' } = router.query

    const mutation = useMutation(async () => {
        // ここで退会処理を行う
        const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/leave/confirm`, {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json',
            },
            body: JSON.stringify({
                userId,
                selectedReason,
                otherReason
            })
        });

        if (!response.ok) {
            throw new Error('退会処理に失敗しました。');
        }

        // 特定のキーをlocalStorageから削除
        localStorage.removeItem('user');
        // 全てのキャッシュを削除
        queryClient.clear();
    }, {
        onSuccess: () => {
            // 退会完了後、完了ページに遷移
            router.push('/leave/complete');
        },
        onError: (error: unknown) => {
            if (error instanceof Error) {
            console.error(error.message);
        } else {
            console.error('不明なエラーが発生しました。');
             }
        }
    });

    const handleLeave = () => {
        mutation.mutate();
    };

    return (
        <div className="container">
            <h1>退会理由</h1>
            <p>{selectedReason === 'other' ? otherReason : selectedReason}</p>
            <p>退会すると、全ての情報が削除されますがよろしいでしょうか？</p>
            <div>
                <button className="leave" onClick={handleLeave} disabled={mutation.isLoading}>
                    {mutation.isLoading ? '処理中...' : '退会する'}
                    </button>
                <button className="cancel" onClick={() => router.push('/leave')}>キャンセル</button>
            </div>
        </div>
    );
};

export default LeaveConfirm;
```
Leaveディレクトリ内にConfirm.cssを作成します。

そして、確認画面のデザインを整えます。

```css:frontend/src/components/Leave/Confirm.css
/* 全体のスタイル */
body {
  font-family: Arial, sans-serif;
  margin: 0;
  padding: 0;
  background-color: #f9f9f9;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 70vh; /* 縦方向の中央揃え */
}

/* タイトルのスタイル */
h1 {
  margin-bottom: 30px;
  text-align: center;
  margin-top: 50px;
  font-size: 28px;
  font-weight: bold;
  text-align: center;
}

/* フォームの全体設定 */
.container {
  display: flex;
  flex-direction: column;
  align-items: center;
  max-width: 800px;
  width: 90%;
  margin: 0 auto;
  padding: 20px;
  background-color: white;
  border: 1px solid #ddd;
  border-radius: 8px;
  box-sizing: border-box;
  font-size: 16px;
  resize: none; /* サイズ変更を無効化 */
}

/* エラーメッセージのスタイル */
p {
  width: 600px;
  text-align: center;
  color: red;
  font-size: 14px;
  margin: 10px 0;
}

/* ボタンのスタイル */
button.leave {
  width: 150px; 
  padding: 10px;
  margin: 10px 5px; /* ボタンの余白を調整 */
  border: none;
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  background-color: red;
  border-radius: 6px;
  transition: background-color 0.3s ease;
}

button.leave:hover {
  background-color: darkred;
}

/* キャンセルリンク */
button.cancel  {
  width: 150px; 
  padding: 10px;
  margin: 10px 5px; /* ボタンの余白を調整 */
  border: none;
  color: white;
  cursor: pointer;
  font-size: 16px;
  font-weight: bold;
  background-color: rgb(167, 160, 160);
  border-radius: 6px;
  transition: background-color 0.3s ease;
}

button.cancel:hover {
  color: gray;
}
```
PHPとの連携が出来ていれば、退会理由の下に前ページで選んだ理由の内容が表示されるようになっていますが、

今は、なされていないので、下のようなデザインになります。

![新規メモ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/de303913-f665-b7cd-bc3e-d6561838e4bb.jpeg)


## ルーティングの設定

この「退会する」ボタンを押した時、ユーザーのIDや登録情報を削除するようにするには、

通常、退会の処理はAPIとして実装されることが多いので、routes/api.phpにコードを追加することをお勧めします。

```php:laravel/routes/api.php

use App\Http\Controllers\LeaveConfirmController;

Route::post('/leave/confirm', [LeaveConfirmController::class, 'leaveConfirm']);
```
## コントローラーの作成

次に、退会処理を行うコントローラーを作成します。LeaveConfirmControllerを作成して、その中に退会処理のロジックを記述します。
```
$ php artisan make:controller LeaveConfirmController
```
app/Http/Controllersディレクトリ内にLeaveConfirmController.phpというファイルが作成されます。

```php:laravel/app/Http/Controllers/LeaveConfirmController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use App\Models\User;

class LeaveConfirmController extends Controller
{
    public function leaveConfirm(Request $request)
    {
        // バリデーション
        $validated = $request->validate([
            'userId' => 'required|integer',
            'selectedReason' => 'required|string',
            'otherReason' => 'nullable|string',
        ]);

        try {
            $userId = $validated['userId'];
            $selectedReason = $validated['selectedReason'];
            $otherReason = $validated['otherReason'] ?? null;

            // 退会理由をleaveconfirm_reasonsテーブルに保存
            DB::table('leaveconfirm_reasons')->insert([
                'user_id' => $userId,
                'reason' => $selectedReason,
                'other_reason' => $otherReason,
                'created_at' => now(),
                'updated_at' => now(),
            ]);

            // ユーザー情報を削除
            $user = User::find($userId);
            if ($user) {
                $user->delete();
            }

            return response()->json([
                'status' => 'success',
                'message' => '退会理由が登録され、ユーザーが削除されました。',
            ]);

        } catch (\Exception $e) {
            Log::error('退会処理エラー: ' . $e->getMessage());

            return response()->json([
                'status' => 'error',
                'message' => '退会処理に失敗しました。再度お試しください。',
            ], 500);
        }
    }
}
```
### LeaveConfirmReason モデルの定義

まず、LeaveConfirmReason モデルを作成する必要があります。
```
$ php artisan make:model LeaveConfirmReason
```
app/Modelsディレクトリ内に、LeaveConfirmReason.phpファイルができるので、

そこに、以下のように設定します。

```php:laravel/app/Models/LeaveConfirmReason.php
<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;

class LeaveConfirmReason extends Model
{
    use HasFactory;

    // テーブル名の指定
    protected $table = 'leaveconfirm_reasons';

    // マスアサインメント可能なカラム
    protected $fillable = [
        'user_id',
        'reason',
        'other_reason',
    ];

    // タイムスタンプを使う場合
    public $timestamps = true;
}
```
### コントローラーでの LeaveConfirmReason モデルの使用

次に、退会理由を leaveconfirm_reasons テーブルに挿入するために、LeaveConfirmController で LeaveConfirmReason モデルを使用します。Eloquent を使うと、データの挿入が非常に簡単になります。

先ほど、作成した
app/Http/Controllers/LeaveConfirmController.php のコードの更新をします。

```diff_php:laravel/app/Http/Controllers/LeaveConfirmController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
- use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
+ use App\Models\LeaveConfirmReason;
use App\Models\User;

class LeaveConfirmController extends Controller
{
    public function leaveConfirm(Request $request)
    {
        // バリデーション
        $validated = $request->validate([
            'userId' => 'required|integer',
            'selectedReason' => 'required|string',
            'otherReason' => 'nullable|string',
        ]);

        try {
            $userId = $validated['userId'];
            $selectedReason = $validated['selectedReason'];
            $otherReason = $validated['otherReason'] ?? null;

- // 退会理由をleaveconfirm_reasonテーブルに保存
- DB::table('leaveconfirm_reasons')->insert([
- 'user_id' => $userId,
- 'reason' => $selectedReason,
- 'other_reason' => $otherReason,
- 'created_at' => now(),
- 'updated_at' => now(),
- ]);

+ // 退会理由をleaveconfirm_reasonsテーブルにEloquentで挿入
+ LeaveConfirmReason::create([
+ 'user_id' => $userId,
+ 'reason' => $selectedReason,
+ 'other_reason' => $otherReason,
+ ]);

            // ユーザー情報を削除
            $user = User::find($userId);
            if ($user) {
                $user->delete();  // ユーザー情報の削除
            }

            return response()->json([
                'status' => 'success',
                'message' => '退会理由が登録され、ユーザーが削除されました。',
            ]);

        } catch (\Exception $e) {
            Log::error('退会処理エラー: ' . $e->getMessage());

            return response()->json([
                'status' => 'error',
                'message' => '退会処理に失敗しました。再度お試しください。',
            ], 500);
        }
    }
}
```
### LeaveConfirmReason::create() メソッドについて

LeaveConfirmReason::create() は、Eloquentの「マスアサインメント」に基づいて、指定された属性を一度に挿入します。

この方法では、fillable プロパティに指定したカラム（user_id, reason, other_reason）だけが挿入対象になります。それ以外のカラム（例えば、id や created_at, updated_at など）は自動的に管理されます。

### マスアサインメントを許可するカラム
$fillable プロパティに指定したカラムのみが、create() メソッドを使用して一度に挿入されることができます。

これにより、悪意のあるユーザーが不正にデータを挿入するのを防げます。

今回の場合、user_id, reason, other_reason の3つのカラムがマスアサインメント可能です。

### timestamps（自動で created_at, updated_at）の使用

LaravelのEloquentでは、timestamps を自動的に管理します。

created_at と updated_at のカラムは、特に設定しなくても自動的に管理されます。

（$timestamps = true の場合）。ですので、これらのカラムを手動で設定する必要はありません。

## マイグレーションの作成
もしまだleaveconfirm_reasonsテーブルが存在していない場合は、マイグレーションを作成してテーブルを作成します。
```
$ php artisan make:migration create_leaveconfirm_reasons_table
```
database/migrationsディレクトリ内に、XXXX_XX_XX_XXXXX_create_leaveconfirm_reasons_table.phpというファイルが作成されるので、

そこに以下を追加します。

```diff_php:laravel/database/migrations/XXXX_XX_XX_XXXXX_create_leaveconfirm_reasons_table.php）
<?php

use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up()
    {
        Schema::create('leaveconfirm_reasons', function (Blueprint $table) {
            $table->id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('reason');
+ $table->text('other_reason')->nullable();
            $table->timestamps();

+ // 外部キーの設定（任意）
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('leaveconfirm_reasons');
    }
}
```
### 実行結果

退会処理を行うと、LeaveConfirmReason::create() によって leaveconfirm_reasons テーブルに退会理由が保存されます。

その後、User::find($userId)->delete() によって、指定された userId を持つユーザー情報が users テーブルから削除されます。

その後、マイグレーションを実行します。
```
$ php artisan migrate
```

### CORSの設定（必要な場合）
フロントエンドとバックエンドが別のドメインやポートで動作している場合、CORS（クロスオリジンリソースシェアリング）を設定する必要があります。Laravelでは、CORSの設定はapp/Http/Middleware/HandleCors.phpで管理されています。

もし、CORSの問題が発生する場合は、以下を設定してみてください。

config/cors.phpの設定を確認し、適切な設定を行います。

# 3.退会完了ページ

再度、フロントエンド側に戻り、Leaveディレクトリ内にComplete.tsxファイルを作り、退会手続きが完了したことを告げるページを作成します。

```typescript:frontend/src/components/Leave/Complete.tsx
import React from 'react';

const LeaveComplete: React.FC = () => {
    return (
        <div className="container">
        <div className="header">
            <h1>退会手続き完了</h1>
        </div>
        <div className="content">
            <p>退会手続きが完了しました。</p>
            <p>またのご利用をお待ちしています。</p>
        </div>
        <div>
            <a href="/login" className="primary-button">ログイン画面へ</a>
        </div>
        </div>
    );
};

export default LeaveComplete;

```
そして、退会完了画面のデザインを作成するために、Leaveディレクトリ内にComplete.cssファイルを作成します。

```css:frontend/src/components/Leave/Complete.css
/* 全体のスタイル */
body {
    font-family: Arial, sans-serif;
    margin: 0;
    padding: 0;
    background-color: #f9f9f9;
    display: flex;
    justify-content: center;
    align-items: center;
    min-height: 70vh; /* 縦方向の中央揃え */
  }
  
  /* タイトルのスタイル */
  h1 {
    margin-bottom: 30px;
    text-align: center;
    margin-top: 50px;
    font-size: 28px;
    font-weight: bold;
    text-align: center;
  }
  
  /* フォームの全体設定 */
  .container {
    display: flex;
    flex-direction: column;
    align-items: center;
    max-width: 800px;
    width: 90%;
    margin: 0 auto ;
    padding: 20px;
    background-color: white;
    border: 1px solid #ddd;
    border-radius: 8px;
    box-sizing: border-box;
    font-size: 16px;
    resize: none; /* サイズ変更を無効化 */
  }
  
  .container > div:last-child {
    margin-top: 38px; /* ボタン部分全体を下げる */
    margin-bottom: 38px;
  }
  
  /* エラーメッセージのスタイル */
  p {
    width: 600px;
    text-align: center;
    color: red;
    font-size: 16px;
    margin: 10px 0 10px; /* 下方向に30pxの余白を追加 */
  }
  
  /* キャンセルリンク */
  .a, a {
    color: white;
    font-size: 14px;
    text-decoration: none; /* 下線を消す */
    width: 150px; 
    padding: 10px;
    margin: 10px 5px; /* ボタンの余白を調整 */
    border: none;
    cursor: pointer;
    font-size: 16px;
    font-weight: bold;
    background-color: rgb(167, 160, 160);
    border-radius: 6px;
    transition: background-color 0.3s ease;
  }
  
  .a, a:hover {
    background-color: gray;
  }
```

完了画面は、こうなります。

![新規メモ.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3945016/e6fce80f-19fd-d04e-05ef-33ea95d68d25.jpeg)


ここまでは、ページの部品であるコンポーネントをただ作成しただけです。

次回は、この作成したコンポーネントを動かす方法を書いていきます。↓

https://qiita.com/makkyo-git/items/72654e569ea9518128fe