（覚書）PHPのファイルアップロード（スクラッチや、コードイグナイター＋Smarty）

- フォームの追加要素：
enctype="multipart/form-data" をフォームに追加
ファイル選択input要素
ファイル形式・サイズ制限の表示

- 確認画面での表示
アップロードファイル名の表示
「なし」の場合の表示

- ファイル保存場所
uploads/ ディレクトリ（自動作成）
重複を避けるタイムスタンプ付きファイル名

- データベース保存
uploaded_file カラムにファイル名のみ保存
NULLも許可（ファイルなしの場合）

### 設定可能な項目
アップロード先: UPLOAD_DIR 定数
ファイルサイズ制限: MAX_FILE_SIZE 定数
許可する拡張子: ALLOWED_EXTENSIONS 配列

定数はconstキーワードでも定義できますが、defineの方が柔軟性があります（実行時に動的な値も設定可能）。

### セキュリティ対策
ファイル拡張子の検証
ファイルサイズ制限
ファイル名のサニタイズ
SQLインジェクション対策

### 使用方法
データベース設定を修正
テーブル作成（コメント内のSQL実行）
uploads/ ディレクトリの書き込み権限設定
必要に応じて設定値を調整

```php
<?php
// config.php - 設定ファイル
define('UPLOAD_DIR', 'uploads/'); // アップロード先ディレクトリ
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5MB制限
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx']); //許可するファイル拡張子を配列で設定

// データベース接続設定
$host = 'localhost';
$dbname = 'your_database';
$username = 'your_username';
$password = 'your_password';

try {
    $pdo = new PDO("mysql:host=$host;dbname=$dbname;charset=utf8", $username, $password);
    $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
} catch(PDOException $e) {
    die("データベース接続エラー: " . $e->getMessage());
}
/*
try-catch文でデータベース接続を試行
PDOオブジェクトを作成してMySQLに接続
*/

/*
アップロードディレクトリが存在しない場合は作成
0755は権限設定（読み書き実行可能）
trueは再帰的作成を許可
*/
if (!file_exists(UPLOAD_DIR)) {
    mkdir(UPLOAD_DIR, 0755, true);
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ファイルアップロード付きフォーム</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; }
        .form-group { margin-bottom: 15px; }
        label { display: block; margin-bottom: 5px; font-weight: bold; }
        input[type="text"], input[type="email"], textarea, input[type="file"] {
            width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px;
        }
        .btn { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; }
        .btn:hover { background: #0056b3; }
        .error { color: red; margin-top: 5px; }
        .success { color: green; margin-top: 5px; }
        .file-info { background: #f8f9fa; padding: 10px; border-radius: 4px; margin-top: 10px; }
    </style>
</head>
<body>

<?php
// フォーム処理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $errors = [];
    $success = false;
    
    // 基本フィールドの検証
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $message = trim($_POST['message'] ?? '');
    
    if (empty($name)) $errors['name'] = '名前を入力してください';
    if (empty($email)) $errors['email'] = 'メールアドレスを入力してください';
    if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors['email'] = '有効なメールアドレスを入力してください';
    if (empty($message)) $errors['message'] = 'メッセージを入力してください';
    
    // ファイルアップロード処理
    $uploaded_filename = null;
    if (isset($_FILES['upload_file']) && $_FILES['upload_file']['error'] !== UPLOAD_ERR_NO_FILE) {
        $file = $_FILES['upload_file'];
        
        // ファイルアップロードエラーチェック
        if ($file['error'] !== UPLOAD_ERR_OK) {
            $errors['file'] = 'ファイルアップロードエラーが発生しました';
        } else {
            // ファイルサイズチェック
            if ($file['size'] > MAX_FILE_SIZE) {
                $errors['file'] = 'ファイルサイズが大きすぎます（最大5MB）';
            }
            
            // 拡張子チェック
            $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
            if (!in_array($file_extension, ALLOWED_EXTENSIONS)) {
                $errors['file'] = '許可されていないファイル形式です';
            }
            
            // ファイル名の重複を避けるため、タイムスタンプ付きのファイル名を生成
            if (empty($errors['file'])) {
                $original_name = pathinfo($file['name'], PATHINFO_FILENAME);
                $safe_filename = preg_replace('/[^a-zA-Z0-9_-]/', '_', $original_name);
                $uploaded_filename = date('Y-m-d_H-i-s') . '_' . $safe_filename . '.' . $file_extension;
                $upload_path = UPLOAD_DIR . $uploaded_filename;
                
                if (!move_uploaded_file($file['tmp_name'], $upload_path)) {
                    $errors['file'] = 'ファイルの保存に失敗しました';
                    $uploaded_filename = null;
                }
            }
        }
    }
    
    // 確認画面の表示
    if (empty($errors) && isset($_POST['action']) && $_POST['action'] === 'confirm') {
        ?>
        <h2>入力内容確認</h2>
        <div class="file-info">
            <p><strong>名前:</strong> <?php echo htmlspecialchars($name); ?></p>
            <p><strong>メールアドレス:</strong> <?php echo htmlspecialchars($email); ?></p>
            <p><strong>メッセージ:</strong><br><?php echo nl2br(htmlspecialchars($message)); ?></p>
            <?php if ($uploaded_filename): ?>
                <p><strong>アップロードファイル:</strong> <?php echo htmlspecialchars($_FILES['upload_file']['name']); ?></p>
            <?php else: ?>
                <p><strong>アップロードファイル:</strong> なし</p>
            <?php endif; ?>
        </div>
        
        <form method="post" style="margin-top: 20px;">
            <input type="hidden" name="name" value="<?php echo htmlspecialchars($name); ?>">
            <input type="hidden" name="email" value="<?php echo htmlspecialchars($email); ?>">
            <input type="hidden" name="message" value="<?php echo htmlspecialchars($message); ?>">
            <input type="hidden" name="uploaded_filename" value="<?php echo htmlspecialchars($uploaded_filename); ?>">
            <input type="hidden" name="action" value="submit">
            
            <button type="submit" class="btn">送信する</button>
            <button type="button" class="btn" onclick="history.back()" style="background: #6c757d;">戻る</button>
        </form>
        <?php
    }
    // データベースへの保存
    elseif (empty($errors) && isset($_POST['action']) && $_POST['action'] === 'submit') {
        try {
            $stmt = $pdo->prepare("INSERT INTO contact_form (name, email, message, uploaded_file, created_at) VALUES (?, ?, ?, ?, NOW())");
            $stmt->execute([$name, $email, $message, $uploaded_filename]);
            $success = true;
        } catch (PDOException $e) {
            $errors['db'] = 'データベースエラーが発生しました';
        }
    }
}
?>

<?php if (isset($success) && $success): ?>
    <div class="success">
        <h2>送信完了</h2>
        <p>お問い合わせを受け付けました。ありがとうございます。</p>
        <a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btn">新規入力</a>
    </div>
<?php else: ?>
    <h2>お問い合わせフォーム</h2>
    
    <?php if (isset($errors['db'])): ?>
        <div class="error"><?php echo $errors['db']; ?></div>
    <?php endif; ?>
    
    <form method="post" enctype="multipart/form-data">
        <div class="form-group">
            <label for="name">名前 *</label>
            <input type="text" id="name" name="name" value="<?php echo htmlspecialchars($_POST['name'] ?? ''); ?>">
            <?php if (isset($errors['name'])): ?>
                <div class="error"><?php echo $errors['name']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group">
            <label for="email">メールアドレス *</label>
            <input type="email" id="email" name="email" value="<?php echo htmlspecialchars($_POST['email'] ?? ''); ?>">
            <?php if (isset($errors['email'])): ?>
                <div class="error"><?php echo $errors['email']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group">
            <label for="message">メッセージ *</label>
            <textarea id="message" name="message" rows="5"><?php echo htmlspecialchars($_POST['message'] ?? ''); ?></textarea>
            <?php if (isset($errors['message'])): ?>
                <div class="error"><?php echo $errors['message']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group">
            <label for="upload_file">ファイル添付（任意）</label>
            <input type="file" id="upload_file" name="upload_file" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx">
            <small>最大5MB、形式: JPG, PNG, GIF, PDF, DOC, DOCX</small>
            <?php if (isset($errors['file'])): ?>
                <div class="error"><?php echo $errors['file']; ?></div>
            <?php endif; ?>
        </div>
        
        <div class="form-group">
            <input type="hidden" name="action" value="confirm">
            <button type="submit" class="btn">確認画面へ</button>
        </div>
    </form>
<?php endif; ?>

</body>
</html>

<?php
// データベーステーブル作成用SQL（初回実行時）
/*
CREATE TABLE contact_form (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,
    uploaded_file VARCHAR(255) NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
*/
?>

<?php
// ファイル管理用のヘルパー関数

/**
 * アップロードされたファイルの情報を取得
 */
function getUploadedFileInfo($filename) {
    if (!$filename || !file_exists(UPLOAD_DIR . $filename)) {
        return null;
    }
    
    $filepath = UPLOAD_DIR . $filename;
    return [
        'name' => $filename,
        'size' => filesize($filepath),
        'modified' => filemtime($filepath),
        'extension' => strtolower(pathinfo($filename, PATHINFO_EXTENSION))
    ];
}

/**
 * ファイルサイズを読みやすい形式に変換
 */
function formatFileSize($bytes) {
    $units = ['B', 'KB', 'MB', 'GB'];
    for ($i = 0; $bytes > 1024 && $i < count($units) - 1; $i++) {
        $bytes /= 1024;
    }
    return round($bytes, 2) . ' ' . $units[$i];
}

/**
 * ファイルダウンロード用のスクリプト（別ファイルとして保存）
 */
// download.php として保存
/*
<?php
session_start();

// セキュリティチェック（ログイン状態など）
if (!isset($_SESSION['user_id'])) {
    http_response_code(403);
    exit('アクセスが拒否されました');
}

$filename = $_GET['file'] ?? '';
if (!$filename) {
    http_response_code(400);
    exit('ファイル名が指定されていません');
}

// ファイルパスの検証
$filepath = UPLOAD_DIR . basename($filename);
if (!file_exists($filepath)) {
    http_response_code(404);
    exit('ファイルが見つかりません');
}

// ファイルダウンロード
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$mime_type = finfo_file($finfo, $filepath);
finfo_close($finfo);

header('Content-Type: ' . $mime_type);
header('Content-Disposition: attachment; filename="' . basename($filename) . '"');
header('Content-Length: ' . filesize($filepath));
readfile($filepath);
exit;
?>
*/
?>
```


## スクラッチPHPでファイルアップロード機能付きフォームの実装

このコードは1つのファイル（index.php）

### 1. **フォーム処理の流れ**
```
入力フォーム → 確認画面 → 送信完了
     ↓           ↓         ↓
   action=confirm → action=submit → 完了表示
```

### 2. **ファイルアップロードの核心部分**

#### HTMLフォーム:
```html
<form method="post" enctype="multipart/form-data">
    <input type="file" name="upload_file">
</form>
```

#### PHP処理:
```php
// $_FILES配列でファイル情報取得
$file = $_FILES['upload_file'];

// エラーチェック
if ($file['error'] !== UPLOAD_ERR_OK) {
    // エラー処理
}

// ファイル移動
move_uploaded_file($file['tmp_name'], $upload_path);
```

### 3. **セキュリティ対策**

1. **ファイル検証**:
   - 拡張子チェック
   - ファイルサイズ制限
   - アップロードエラー確認

2. **ファイル名安全化**:
   ```php
   $safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '_', $filename);
   $new_filename = date('Y-m-d_H-i-s') . '_' . $safe_name;
   ```

3. **XSS対策**:
   ```php
   function h($text) {
       return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
   }
   ```

### 4. **データ保存方法**

このサンプルではCSVファイルに保存していますが、実際は：

**DB保存の場合**:
```php
$pdo = new PDO($dsn, $user, $pass);
$stmt = $pdo->prepare("INSERT INTO contacts (name, email, message, file_name) VALUES (?, ?, ?, ?)");
$stmt->execute([$name, $email, $message, $uploaded_file]);
```

### 5. **重要なポイント**

1. **enctype属性必須**:
   ```html
   <form enctype="multipart/form-data">
   ```

2. **$_FILES配列の構造**:
   ```php
   $_FILES['upload_file'] = [
       'name' => '元のファイル名',
       'tmp_name' => '一時ファイルパス',
       'size' => 'ファイルサイズ',
       'error' => 'エラーコード'
   ];
   ```

3. **セッション活用**:
   - 確認画面でファイル情報保持
   - 送信完了後にクリア

### 6. **動作確認手順**

1. **ファイル配置**: index.phpを配置
2. **権限設定**: uploadsディレクトリ作成＋書き込み権限
3. **動作テスト**: ブラウザでアクセス

### 7. **カスタマイズポイント**

- `ALLOWED_EXTENSIONS`: 許可する拡張子
- `MAX_FILE_SIZE`: ファイルサイズ制限
- `process_submit()`: データ保存方法
- CSS: デザインカスタマイズ


```php
<?php
/*
=====================================
1. index.php - メインファイル
=====================================
*/

// セッション開始
session_start();

// エラー表示設定（開発時のみ）
error_reporting(E_ALL);
ini_set('display_errors', 1);

// 設定
define('UPLOAD_DIR', 'uploads/');
define('MAX_FILE_SIZE', 5 * 1024 * 1024); // 5MB
define('ALLOWED_EXTENSIONS', ['jpg', 'jpeg', 'png', 'gif', 'pdf', 'doc', 'docx', 'txt']);

// アップロードディレクトリ作成
if (!is_dir(UPLOAD_DIR)) {
    mkdir(UPLOAD_DIR, 0755, true);
}

// 変数初期化
$errors = [];
$form_data = [];
$step = 'input'; // input, confirm, complete
$uploaded_file = null;
$original_name = null;

// POST処理
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    $action = $_POST['action'] ?? '';
    
    switch ($action) {
        case 'confirm':
            $result = process_confirm();
            $errors = $result['errors'];
            $form_data = $result['form_data'];
            
            if (empty($errors)) {
                $step = 'confirm';
                $uploaded_file = $result['uploaded_file'];
                $original_name = $result['original_name'];
            }
            break;
            
        case 'submit':
            $result = process_submit();
            if ($result['success']) {
                $step = 'complete';
            } else {
                $errors = $result['errors'];
                $step = 'input';
            }
            break;
            
        case 'back':
            $step = 'input';
            $form_data = [
                'name' => $_POST['name'] ?? '',
                'email' => $_POST['email'] ?? '',
                'message' => $_POST['message'] ?? ''
            ];
            break;
    }
}

/**
 * 確認処理
 */
function process_confirm() {
    $result = [
        'errors' => [],
        'form_data' => [],
        'uploaded_file' => null,
        'original_name' => null
    ];
    
    // フォームデータ取得・検証
    $name = trim($_POST['name'] ?? '');
    $email = trim($_POST['email'] ?? '');
    $message = trim($_POST['message'] ?? '');
    
    $result['form_data'] = [
        'name' => $name,
        'email' => $email,
        'message' => $message
    ];
    
    // バリデーション
    if (empty($name)) {
        $result['errors']['name'] = '名前を入力してください';
    } elseif (mb_strlen($name) > 100) {
        $result['errors']['name'] = '名前は100文字以内で入力してください';
    }
    
    if (empty($email)) {
        $result['errors']['email'] = 'メールアドレスを入力してください';
    } elseif (!filter_var($email, FILTER_VALIDATE_EMAIL)) {
        $result['errors']['email'] = '正しいメールアドレスを入力してください';
    }
    
    if (empty($message)) {
        $result['errors']['message'] = 'メッセージを入力してください';
    } elseif (mb_strlen($message) > 1000) {
        $result['errors']['message'] = 'メッセージは1000文字以内で入力してください';
    }
    
    // ファイルアップロード処理
    if (empty($result['errors'])) {
        $upload_result = handle_file_upload();
        if ($upload_result['error']) {
            $result['errors']['file'] = $upload_result['error'];
        } else {
            $result['uploaded_file'] = $upload_result['file_name'];
            $result['original_name'] = $upload_result['original_name'];
            
            // セッションに保存
            $_SESSION['uploaded_file'] = $upload_result['file_name'];
            $_SESSION['original_name'] = $upload_result['original_name'];
        }
    }
    
    return $result;
}

/**
 * 送信処理
 */
function process_submit() {
    $result = ['success' => false, 'errors' => []];
    
    // データ取得
    $name = $_POST['name'] ?? '';
    $email = $_POST['email'] ?? '';
    $message = $_POST['message'] ?? '';
    $uploaded_file = $_SESSION['uploaded_file'] ?? null;
    
    // 簡単なデータ保存（実際はDBに保存）
    $data = [
        'name' => $name,
        'email' => $email,
        'message' => $message,
        'uploaded_file' => $uploaded_file,
        'datetime' => date('Y-m-d H:i:s')
    ];
    
    // CSVファイルに保存（簡易的なデータ保存）
    $csv_file = 'data.csv';
    $csv_data = [
        $data['datetime'],
        $data['name'],
        $data['email'],
        str_replace(["\r\n", "\r", "\n"], ' ', $data['message']), // 改行除去
        $data['uploaded_file'] ?? ''
    ];
    
    if (file_put_contents($csv_file, '"' . implode('","', $csv_data) . '"' . PHP_EOL, FILE_APPEND | LOCK_EX)) {
        $result['success'] = true;
        // セッションクリア
        unset($_SESSION['uploaded_file'], $_SESSION['original_name']);
    } else {
        $result['errors']['save'] = 'データの保存に失敗しました';
    }
    
    return $result;
}

/**
 * ファイルアップロード処理
 */
function handle_file_upload() {
    $result = [
        'error' => null,
        'file_name' => null,
        'original_name' => null
    ];
    
    // ファイルが選択されていない場合
    if (!isset($_FILES['upload_file']) || $_FILES['upload_file']['error'] === UPLOAD_ERR_NO_FILE) {
        return $result; // エラーなし、ファイルなし
    }
    
    $file = $_FILES['upload_file'];
    
    // アップロードエラーチェック
    if ($file['error'] !== UPLOAD_ERR_OK) {
        $result['error'] = get_upload_error_message($file['error']);
        return $result;
    }
    
    // ファイルサイズチェック
    if ($file['size'] > MAX_FILE_SIZE) {
        $result['error'] = 'ファイルサイズが大きすぎます（最大5MB）';
        return $result;
    }
    
    // 拡張子チェック
    $file_extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION));
    if (!in_array($file_extension, ALLOWED_EXTENSIONS)) {
        $result['error'] = '許可されていないファイル形式です';
        return $result;
    }
    
    // ファイル名生成（重複回避）
    $original_name = $file['name'];
    $safe_name = preg_replace('/[^a-zA-Z0-9._-]/', '_', pathinfo($original_name, PATHINFO_FILENAME));
    $new_filename = date('Y-m-d_H-i-s') . '_' . $safe_name . '.' . $file_extension;
    $upload_path = UPLOAD_DIR . $new_filename;
    
    // ファイル移動
    if (move_uploaded_file($file['tmp_name'], $upload_path)) {
        $result['file_name'] = $new_filename;
        $result['original_name'] = $original_name;
    } else {
        $result['error'] = 'ファイルの保存に失敗しました';
    }
    
    return $result;
}

/**
 * アップロードエラーメッセージ取得
 */
function get_upload_error_message($error_code) {
    $messages = [
        UPLOAD_ERR_INI_SIZE => 'ファイルサイズがPHP設定の制限を超えています',
        UPLOAD_ERR_FORM_SIZE => 'ファイルサイズがフォーム設定の制限を超えています',
        UPLOAD_ERR_PARTIAL => 'ファイルが部分的にしかアップロードされませんでした',
        UPLOAD_ERR_NO_TMP_DIR => 'テンポラリディレクトリが見つかりません',
        UPLOAD_ERR_CANT_WRITE => 'ディスクへの書き込みに失敗しました',
        UPLOAD_ERR_EXTENSION => 'PHPの拡張機能によってアップロードが停止されました'
    ];
    
    return $messages[$error_code] ?? '不明なエラーが発生しました';
}

/**
 * HTMLエスケープ
 */
function h($text) {
    return htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
}
?>

<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ファイルアップロード付きフォーム</title>
    <style>
        * {
            box-sizing: border-box;
        }
        body {
            font-family: 'Hiragino Sans', 'Yu Gothic', Arial, sans-serif;
            margin: 0;
            padding: 20px;
            background: #f5f5f5;
            line-height: 1.6;
        }
        .container {
            max-width: 600px;
            margin: 0 auto;
            background: white;
            padding: 30px;
            border-radius: 8px;
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h1, h2 {
            color: #333;
            text-align: center;
            margin-bottom: 30px;
        }
        h1 {
            border-bottom: 3px solid #007bff;
            padding-bottom: 10px;
        }
        h2 {
            border-bottom: 2px solid #28a745;
            padding-bottom: 10px;
        }
        .step-indicator {
            display: flex;
            justify-content: center;
            margin-bottom: 30px;
            gap: 20px;
        }
        .step {
            padding: 8px 16px;
            border-radius: 20px;
            font-size: 14px;
            font-weight: bold;
        }
        .step.active {
            background: #007bff;
            color: white;
        }
        .step.inactive {
            background: #e9ecef;
            color: #6c757d;
        }
        .form-group {
            margin-bottom: 20px;
        }
        label {
            display: block;
            margin-bottom: 8px;
            font-weight: bold;
            color: #555;
        }
        .required::after {
            content: " *";
            color: red;
        }
        input[type="text"],
        input[type="email"],
        textarea,
        input[type="file"] {
            width: 100%;
            padding: 12px;
            border: 1px solid #ddd;
            border-radius: 4px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        input[type="text"]:focus,
        input[type="email"]:focus,
        textarea:focus {
            outline: none;
            border-color: #007bff;
            box-shadow: 0 0 5px rgba(0,123,255,0.3);
        }
        textarea {
            resize: vertical;
            min-height: 100px;
        }
        .btn {
            padding: 12px 24px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 16px;
            transition: all 0.3s;
            text-decoration: none;
            display: inline-block;
            text-align: center;
        }
        .btn-primary {
            background: #007bff;
            color: white;
        }
        .btn-primary:hover {
            background: #0056b3;
        }
        .btn-secondary {
            background: #6c757d;
            color: white;
        }
        .btn-secondary:hover {
            background: #545b62;
        }
        .btn-success {
            background: #28a745;
            color: white;
        }
        .btn-success:hover {
            background: #1e7e34;
        }
        .btn-full {
            width: 100%;
        }
        .btn-group {
            display: flex;
            gap: 15px;
        }
        .btn-group .btn {
            flex: 1;
        }
        .error {
            color: #dc3545;
            font-size: 14px;
            margin-top: 5px;
            display: block;
        }
        .file-info {
            font-size: 12px;
            color: #666;
            margin-top: 5px;
            line-height: 1.4;
        }
        .confirm-box {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
            margin-bottom: 30px;
            border: 1px solid #dee2e6;
        }
        .confirm-item {
            margin-bottom: 15px;
            padding-bottom: 15px;
            border-bottom: 1px solid #dee2e6;
        }
        .confirm-item:last-child {
            margin-bottom: 0;
            padding-bottom: 0;
            border-bottom: none;
        }
        .confirm-label {
            font-weight: bold;
            margin-bottom: 8px;
            color: #495057;
            font-size: 14px;
        }
        .confirm-value {
            padding: 10px;
            background: white;
            border-radius: 4px;
            border: 1px solid #ced4da;
            min-height: 20px;
            line-height: 1.5;
        }
        .success-box {
            background: #d4edda;
            color: #155724;
            padding: 30px;
            border-radius: 8px;
            text-align: center;
            border: 1px solid #c3e6cb;
        }
        .success-icon {
            font-size: 48px;
            margin-bottom: 20px;
        }
        .file-icon {
            color: #007bff;
            margin-right: 5px;
        }
    </style>
</head>
<body>
    <div class="container">
        <!-- ステップインジケーター -->
        <div class="step-indicator">
            <div class="step <?php echo $step === 'input' ? 'active' : 'inactive'; ?>">1. 入力</div>
            <div class="step <?php echo $step === 'confirm' ? 'active' : 'inactive'; ?>">2. 確認</div>
            <div class="step <?php echo $step === 'complete' ? 'active' : 'inactive'; ?>">3. 完了</div>
        </div>

        <?php if ($step === 'input'): ?>
            <!-- 入力フォーム -->
            <h1>お問い合わせフォーム</h1>
            
            <form method="post" enctype="multipart/form-data">
                <div class="form-group">
                    <label for="name" class="required">お名前</label>
                    <input type="text" id="name" name="name" value="<?php echo h($form_data['name'] ?? ''); ?>" placeholder="山田太郎">
                    <?php if (isset($errors['name'])): ?>
                        <span class="error"><?php echo h($errors['name']); ?></span>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="email" class="required">メールアドレス</label>
                    <input type="email" id="email" name="email" value="<?php echo h($form_data['email'] ?? ''); ?>" placeholder="example@example.com">
                    <?php if (isset($errors['email'])): ?>
                        <span class="error"><?php echo h($errors['email']); ?></span>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="message" class="required">お問い合わせ内容</label>
                    <textarea id="message" name="message" rows="6" placeholder="お問い合わせ内容をご記入ください"><?php echo h($form_data['message'] ?? ''); ?></textarea>
                    <?php if (isset($errors['message'])): ?>
                        <span class="error"><?php echo h($errors['message']); ?></span>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <label for="upload_file">ファイル添付（任意）</label>
                    <input type="file" id="upload_file" name="upload_file" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.txt">
                    <div class="file-info">
                        ・最大ファイルサイズ: 5MB<br>
                        ・対応形式: JPG, PNG, GIF, PDF, DOC, DOCX, TXT
                    </div>
                    <?php if (isset($errors['file'])): ?>
                        <span class="error"><?php echo h($errors['file']); ?></span>
                    <?php endif; ?>
                </div>
                
                <div class="form-group">
                    <input type="hidden" name="action" value="confirm">
                    <button type="submit" class="btn btn-primary btn-full">確認画面へ</button>
                </div>
            </form>

        <?php elseif ($step === 'confirm'): ?>
            <!-- 確認画面 -->
            <h2>入力内容確認</h2>
            
            <div class="confirm-box">
                <div class="confirm-item">
                    <div class="confirm-label">お名前</div>
                    <div class="confirm-value"><?php echo h($form_data['name']); ?></div>
                </div>
                
                <div class="confirm-item">
                    <div class="confirm-label">メールアドレス</div>
                    <div class="confirm-value"><?php echo h($form_data['email']); ?></div>
                </div>
                
                <div class="confirm-item">
                    <div class="confirm-label">お問い合わせ内容</div>
                    <div class="confirm-value"><?php echo nl2br(h($form_data['message'])); ?></div>
                </div>
                
                <div class="confirm-item">
                    <div class="confirm-label">添付ファイル</div>
                    <div class="confirm-value">
                        <?php if ($uploaded_file && $original_name): ?>
                            <span class="file-icon">📎</span><?php echo h($original_name); ?>
                        <?php else: ?>
                            なし
                        <?php endif; ?>
                    </div>
                </div>
            </div>
            
            <div class="btn-group">
                <form method="post" style="flex: 1;">
                    <input type="hidden" name="name" value="<?php echo h($form_data['name']); ?>">
                    <input type="hidden" name="email" value="<?php echo h($form_data['email']); ?>">
                    <input type="hidden" name="message" value="<?php echo h($form_data['message']); ?>">
                    <input type="hidden" name="action" value="submit">
                    <button type="submit" class="btn btn-success btn-full">送信する</button>
                </form>
                
                <form method="post" style="flex: 1;">
                    <input type="hidden" name="name" value="<?php echo h($form_data['name']); ?>">
                    <input type="hidden" name="email" value="<?php echo h($form_data['email']); ?>">
                    <input type="hidden" name="message" value="<?php echo h($form_data['message']); ?>">
                    <input type="hidden" name="action" value="back">
                    <button type="submit" class="btn btn-secondary btn-full">戻って修正</button>
                </form>
            </div>

        <?php elseif ($step === 'complete'): ?>
            <!-- 完了画面 -->
            <div class="success-box">
                <div class="success-icon">✓</div>
                <h2>送信完了</h2>
                <p><strong>お問い合わせを受け付けました。</strong></p>
                <p>ご連絡いただきありがとうございます。<br>
                内容を確認の上、担当者よりご連絡させていただきます。</p>
            </div>
            
            <div style="text-align: center; margin-top: 30px;">
                <a href="<?php echo $_SERVER['PHP_SELF']; ?>" class="btn btn-primary">新規お問い合わせ</a>
            </div>

        <?php endif; ?>
    </div>

    <script>
        // ファイル選択時のプレビュー表示
        document.getElementById('upload_file')?.addEventListener('change', function() {
            const file = this.files[0];
            if (file) {
                const fileInfo = document.querySelector('.file-info');
                const sizeKB = Math.round(file.size / 1024);
                const sizeMB = (file.size / (1024 * 1024)).toFixed(2);
                
                fileInfo.innerHTML += `<br><strong>選択されたファイル:</strong><br>` +
                    `ファイル名: ${file.name}<br>` +
                    `サイズ: ${sizeKB > 1024 ? sizeMB + 'MB' : sizeKB + 'KB'}`;
            }
        });
    </script>
</body>
</html>

<?php
/*
=====================================
補足: data.csv の構造
=====================================
送信日時,名前,メールアドレス,メッセージ,ファイル名

例:
"2025-06-08 15:30:25","山田太郎","yamada@example.com","お問い合わせ内容です","2025-06-08_15-30-25_document.pdf"
*/

/*
=====================================
補足: uploads/ ディレクトリの構造
=====================================
uploads/
├── 2025-06-08_15-30-25_document.pdf
├── 2025-06-08_16-45-12_image.jpg
└── .htaccess (セキュリティ用)

.htaccess 内容:
Order Deny,Allow
Deny from all
<Files "*.jpg">
    Allow from all
</Files>
<Files "*.jpeg">
    Allow from all
</Files>
<Files "*.png">
    Allow from all
</Files>
<Files "*.gif">
    Allow from all
</Files>
<Files "*.pdf">
    Allow from all
</Files>
<Files "*.doc">
    Allow from all
</Files>
<Files "*.docx">
    Allow from all
</Files>
<Files "*.txt">
    Allow from all
</Files>
*/
?>
```

## CodeIgniter、スマーティー、postgreSQL

- アーキテクチャ
MVC（Model-View-Controller）パターンの実装
Smartyテンプレートエンジンの統合
PostgreSQLデータベース使用

- セキュリティ機能
フォームバリデーション（必須チェック、メール形式、文字数制限）
HTMLエスケープ処理
ファイルアップロード制限（形式・サイズ・画像サイズ）
直接アクセス防止

- Controller (Contact.php)：
MVC パターンに従った実装
CodeIgniterの標準ライブラリを使用
ファイルアップロード処理とバリデーション

- Model (Contact_model.php)：
PostgreSQL用のデータベース操作
CRUD操作の基本実装

- View (Smarty テンプレート)：
form.tpl: 入力フォーム
confirm.tpl: 確認画面
complete.tpl: 完了画面

ファイル構成
Controller: メイン処理ロジック
Model: データベース操作
View: Smartyテンプレート3画面
Library: Smarty設定


```php
application/
├── controllers/
│   └── Contact.php
├── models/
│   └── Contact_model.php
├── libraries/
│   └── Smarty.php
├── views/
│   └── templates/
│       └── contact/
│           ├── form.tpl
│           ├── confirm.tpl
│           └── complete.tpl
├── config/
│   ├── autoload.php
│   └── database.php
└── third_party/
    └── smarty/  (Smartyライブラリを配置)

uploads/  (ルートディレクトリに作成される)
```

```php
<?php
/*
==============================================
1. Controller: application/controllers/Contact.php
==============================================
*/
defined('BASEPATH') OR exit('No direct script access allowed');
//CodeIgniterの基本パス定数が定義されているかチェック。未定義の場合は直接アクセスを拒否

class Contact extends CI_Controller {
//ContactクラスをCI_Controllerから継承して定義
    
    public function __construct() { //ContactクラスをCI_Controllerから継承して定義
        parent::__construct(); //コンストラクタ関数の定義
        $this->load->database(); //データベースライブラリを読み込み
        $this->load->helper(['form', 'url', 'file']); //form（フォーム）、url（URL）、file（ファイル）ヘルパーを読み込み
        $this->load->library(['form_validation', 'upload', 'session']); //form（フォーム）、url（URL）、file（ファイル）ヘルパーを読み込み
        $this->load->model('Contact_model'); //Contact_modelを読み込み
        
        // Smarty読み込み
        $this->load->library('smarty'); //Smartyテンプレートエンジンライブラリを読み込み
    }
    
    /**
     * メイン処理
     */
    public function index() { //index()メソッドの定義（デフォルトアクション）
        $data = [
            'errors' => [],
            'form_data' => [],
            'success' => false
        ];
        //index()メソッドの定義（デフォルトアクション）
        
        if ($this->input->method() === 'post') {
        //HTTPリクエストがPOSTかどうかをチェック
            $action = $this->input->post('action');
            //POSTデータから'action'パラメータを取得
            
            switch ($action) {
            //actionの値によって処理を分岐するswitch文開始
                case 'confirm': //actionが'confirm'（確認）の場合
                    $data = $this->_process_confirm(); //確認処理メソッドを呼び出し
                    if (empty($data['errors'])) { 
                    //エラーがない場合は確認画面を表示してreturn
                        $this->_show_confirm($data); 
                        return;
                    }
                    break;
                    
                case 'submit': //actionが'submit'（送信）の場合
                    $data = $this->_process_submit(); //送信処理メソッドを呼び出し
                    if ($data['success']) { //成功の場合は完了画面を表示してreturn
                        $this->_show_complete();
                        return;
                    }
                    break;
            }
        }
        
        $this->_show_form($data); 
        //どのactionにも該当しない場合、またはGETリクエストの場合はフォームを表示
    }
    
    /**
     * 確認処理
     */
    private function _process_confirm() { //プライベートメソッド_process_confirm()の定義
        $data = [
            'errors' => [],
            'form_data' => [],
            'uploaded_file' => null,
            'original_name' => null
        ];
        //結果データ配列の初期化
        
        // バリデーション設定
        $this->form_validation->set_rules('name', '名前', 'required|trim|max_length[100]');
        $this->form_validation->set_rules('email', 'メールアドレス', 'required|valid_email|max_length[255]');
        $this->form_validation->set_rules('message', 'メッセージ', 'required|trim|max_length[1000]');
        /*
            フォームバリデーションルールの設定
            name: 必須、空白除去、最大100文字
            email: 必須、有効なメール形式、最大255文字
            message: 必須、空白除去、最大1000文字
        */
        
        // バリデーション実行とエラー処理
        if (!$this->form_validation->run()) {
            $data['errors'] = $this->form_validation->error_array();
        }
        
        // POSTデータを取得してフォームデータ配列に格納（trim()で空白除去）
        $data['form_data'] = [
            'name' => trim($this->input->post('name')),
            'email' => trim($this->input->post('email')),
            'message' => trim($this->input->post('message'))
        ];
        
        // ファイルアップロード処理を実行し、エラーがあれば配列に格納
        $upload_result = $this->_handle_file_upload();
        if (!empty($upload_result['error'])) {
            $data['errors']['upload_file'] = $upload_result['error'];
        } else {
            $data['uploaded_file'] = $upload_result['file_name'];
            $data['original_name'] = $upload_result['original_name'];
            
            // アップロード成功時はファイル名を保存し、セッションにも保存
            $this->session->set_userdata('upload_file_name', $upload_result['file_name']);
            $this->session->set_userdata('upload_original_name', $upload_result['original_name']);
        }
        
        return $data; //処理結果を返却
    }
    
    /**
     * 送信処理
     */
    private function _process_submit() { //プライベートメソッド_process_submit()の定義
        $data = ['success' => false, 'errors' => []];
        // 結果データ配列の初期化
        $form_data = [
            'name' => $this->input->post('name'),
            'email' => $this->input->post('email'),
            'message' => $this->input->post('message'),
            'uploaded_file' => $this->input->post('uploaded_filename')
        ];
        //POSTデータからフォームデータ配列を作成
        
        // モデルのinsert_contact()メソッドでデータベース保存を試行
        if ($this->Contact_model->insert_contact($form_data)) {
            $data['success'] = true; //成功時はsuccessフラグをtrueに設定
            // セッションからアップロードファイル情報をクリア
            $this->session->unset_userdata(['upload_file_name', 'upload_original_name']);
        } else {
            $data['errors']['db'] = 'データベースエラーが発生しました。';
        }
        
        return $data;
    }
    
    /**
     * ファイルアップロード処理
     */
    private function _handle_file_upload() {
        $result = [
            'error' => null,
            'file_name' => null,
            'original_name' => null
        ]; //結果配列の初期化
        
        // ファイルが選択されていない場合は空の結果を返却
        if (empty($_FILES['upload_file']['name'])) {
            return $result;
        }
        
        // アップロード設定
        $upload_path = './uploads/';
        $config = [
            'upload_path' => $upload_path,
            'allowed_types' => 'jpg|jpeg|png|gif|pdf|doc|docx|txt',
            'max_size' => 5120, // 5MB (KB単位)
            'max_width' => 2048,
            'max_height' => 2048,
            'encrypt_name' => TRUE, // ファイル名を暗号化
            'remove_spaces' => TRUE
        ];
        
        // アップロードディレクトリが存在しない場合は作成（権限755
        if (!is_dir($upload_path)) {
            if (!mkdir($upload_path, 0755, true)) {
                $result['error'] = 'アップロードディレクトリの作成に失敗しました。';
                return $result;
            }
        }
        
        // アップロードライブラリを初期化して実行
        $this->upload->initialize($config);
        
        if (!$this->upload->do_upload('upload_file')) {
            $result['error'] = strip_tags($this->upload->display_errors());
        } else { 
        //成功時はファイル名と元のファイル名を取得
            $upload_data = $this->upload->data();
            $result['file_name'] = $upload_data['file_name'];
            $result['original_name'] = $upload_data['orig_name'];
        }
        
        return $result;
    }
    
    /**
     * フォーム表示
     */
    private function _show_form($data) {
        $this->smarty->assign('errors', $data['errors']);
        $this->smarty->assign('form_data', $data['form_data']);
        $this->smarty->display('contact/form.tpl');
    }
    
    /**
     * 確認画面表示
     */
    private function _show_confirm($data) {
        $this->smarty->assign('form_data', $data['form_data']);
        $this->smarty->assign('uploaded_file', $data['uploaded_file']);
        $this->smarty->assign('original_name', $data['original_name']);
        $this->smarty->display('contact/confirm.tpl');
    }
    
    /**
     * 完了画面表示
     */
    private function _show_complete() {
        $this->smarty->display('contact/complete.tpl');
    }
}

/*
==============================================
2. Model: application/models/Contact_model.php
==============================================
*/
// CodeIgniterの基本パス定数が定義されているかチェック。未定義の場合は直接アクセスを拒否
defined('BASEPATH') OR exit('No direct script access allowed');

// Contact_modelクラスをCI_Modelから継承して定義
class Contact_model extends CI_Model {
    
    private $table = 'contact_form';
    
    public function __construct() {
        parent::__construct();
        $this->load->database();
    }
    
    /**
     * お問い合わせデータ挿入
     */
    public function insert_contact($data) {
        $insert_data = [
            'name' => $data['name'],
            'email' => $data['email'],
            'message' => $data['message'],
            'uploaded_file' => !empty($data['uploaded_file']) ? $data['uploaded_file'] : null,
            'created_at' => date('Y-m-d H:i:s')
        ];
        
        return $this->db->insert($this->table, $insert_data);
    }
    
    /**
     * ID指定でデータ取得
     */
    public function get_contact_by_id($id) {
        $query = $this->db->where('id', $id)->get($this->table);
        return $query->row_array();
    }
    
    /**
     * 全データ取得
     */
    public function get_all_contacts($limit = 100, $offset = 0) {
        $query = $this->db
            ->order_by('created_at', 'DESC')
            ->limit($limit, $offset)
            ->get($this->table);
        
        return $query->result_array();
    }
    
    /**
     * データ件数取得
     */
    public function count_contacts() {
        return $this->db->count_all($this->table);
    }
}

/*
==============================================
3. Smarty Library: application/libraries/Smarty.php
==============================================
*/
// CodeIgniterの基本パス定数が定義されているかチェック。未定義の場合は直接アクセスを拒否
defined('BASEPATH') OR exit('No direct script access allowed');
// Smartyクラスファイルを読み込み（third_partyディレクトリから）
require_once(APPPATH . 'third_party/smarty/Smarty.class.php');

// CI_SmartyクラスをSmartyクラスから継承して定義
class CI_Smarty extends Smarty {
    // コンストラクタ（クラスがインスタンス化された時に自動実行される）
    public function __construct() {
    // 親クラス（Smarty）のコンストラクタを呼び出し
        parent::__construct();
        
       // Smartyのディレクトリ設定
        $this->setTemplateDir(APPPATH . 'views/templates/');  // テンプレートファイル格納ディレクトリ
        $this->setCompileDir(APPPATH . 'views/templates_c/'); // コンパイル済みテンプレート格納ディレクトリ
        $this->setConfigDir(APPPATH . 'views/configs/');     // 設定ファイル格納ディレクトリ
        $this->setCacheDir(APPPATH . 'views/cache/');        // キャッシュファイル格納ディレクトリ
        
        
        // キャッシュ機能をオフに設定（開発時は無効化が推奨）
        $this->setCaching(Smarty::CACHING_OFF);
        
        // 開発環境の場合はデバッグ機能を有効化
        if (ENVIRONMENT === 'development') {
            $this->setDebugging(true);
        }
        
        // CodeIgniterのインスタンスを取得
        // CodeIgniterの共通関数をSmartyテンプレートで使用可能にする
        $CI =& get_instance();
        $this->assign('base_url', base_url());
        $this->assign('site_url', site_url());
        $this->assign('current_url', current_url());
    }
}

/*
==============================================
4. PostgreSQL テーブル作成SQL
==============================================
*/
/*
-- テーブル作成
CREATE TABLE contact_form (
    id SERIAL PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(255) NOT NULL,
    message TEXT NOT NULL,
    uploaded_file VARCHAR(255),
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- インデックス作成
CREATE INDEX idx_contact_form_created_at ON contact_form(created_at DESC);
CREATE INDEX idx_contact_form_email ON contact_form(email);

-- コメント追加
COMMENT ON TABLE contact_form IS 'お問い合わせフォーム';
COMMENT ON COLUMN contact_form.id IS 'ID';
COMMENT ON COLUMN contact_form.name IS '名前';
COMMENT ON COLUMN contact_form.email IS 'メールアドレス';
COMMENT ON COLUMN contact_form.message IS 'メッセージ';
COMMENT ON COLUMN contact_form.uploaded_file IS 'アップロードファイル名';
COMMENT ON COLUMN contact_form.created_at IS '作成日時';
*/

/*
==============================================
5. 設定ファイル: application/config/autoload.php
==============================================
*/
/*
// 以下を追加または編集

// 自動読み込みライブラリ（database: DB接続、session: セッション管理、smarty: テンプレート）
$autoload['libraries'] = array('database', 'session', 'smarty');

// 自動読み込みヘルパー（url: URL生成、form: フォーム作成、file: ファイル操作、security: セキュリティ）
$autoload['helper'] = array('url', 'form', 'file', 'security');

// 自動読み込みモデル（必要に応じて設定）
$autoload['model'] = array();
*/

/*
==============================================
6. データベース設定: application/config/database.php
==============================================
*/
/*
$active_group = 'default';
$query_builder = TRUE;

$db['default'] = array(
    'dsn'       => '',                              // データソース名（PostgreSQL使用時は通常空）
    'hostname'  => 'localhost',                     // データベースサーバーのホスト名
    'username'  => 'your_username',                 // データベースユーザー名
    'password'  => 'your_password',                 // データベースパスワード
    'database'  => 'your_database',                 // データベース名
    'dbdriver'  => 'postgre',                       // データベースドライバー（PostgreSQL用）
    'dbprefix'  => '',                              // テーブル名のプレフィックス
    'pconnect'  => FALSE,                           // 持続的接続の使用可否
    'db_debug'  => (ENVIRONMENT !== 'production'),  // 本番環境以外でデバッグモード有効
    'cache_on'  => FALSE,                           // クエリキャッシュの使用可否
    'cachedir'  => '',                              // キャッシュディレクトリ
    'char_set'  => 'utf8',                          // 文字セット
    'dbcollat'  => 'utf8_general_ci',              // 照合順序
    'swap_pre'  => '',                              // テーブルプレフィックスの置換
    'encrypt'   => FALSE,                           // 接続の暗号化
    'compress'  => FALSE,                           // 接続の圧縮
    'stricton'  => FALSE,                           // 厳密モード
    'failover'  => array(),                         // フェイルオーバー設定
    'save_queries' => TRUE                          // 実行クエリの保存（デバッグ用）
);
*/
?>

<!-- 
==============================================
7. Smarty Template: application/views/templates/contact/form.tpl
============================================== 
-->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>お問い合わせフォーム</title>
    <style>
        * { box-sizing: border-box; }
        body { 
            font-family: 'Hiragino Sans', 'Yu Gothic', Arial, sans-serif; 
            margin: 0; 
            padding: 20px; 
            background-color: #f5f5f5;
        }
        .container { 
            max-width: 600px; 
            margin: 0 auto; 
            background: white; 
            padding: 30px; 
            border-radius: 8px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h2 { 
            color: #333; 
            margin-bottom: 30px; 
            text-align: center;
            border-bottom: 2px solid #007bff;
            padding-bottom: 10px;
        }
        .form-group { 
            margin-bottom: 20px; 
        }
        label { 
            display: block; 
            margin-bottom: 8px; 
            font-weight: bold; 
            color: #555;
        }
        .required::after {
            content: " *";
            color: red;
        }
        input[type="text"], 
        input[type="email"], 
        textarea, 
        input[type="file"] {
            width: 100%; 
            padding: 12px; 
            border: 1px solid #ddd; 
            border-radius: 4px;
            font-size: 14px;
            transition: border-color 0.3s;
        }
        input[type="text"]:focus,
        input[type="email"]:focus,
        textarea:focus {
            outline: none;
            border-color: #007bff;
            box-shadow: 0 0 5px rgba(0,123,255,0.3);
        }
        textarea {
            resize: vertical;
            min-height: 100px;
        }
        .btn { 
            padding: 12px 30px; 
            background: #007bff; 
            color: white; 
            border: none; 
            border-radius: 4px; 
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
            width: 100%;
        }
        .btn:hover { 
            background: #0056b3; 
        }
        .error { 
            color: #dc3545; 
            font-size: 14px; 
            margin-top: 5px;
            display: block;
        }
        .file-info { 
            font-size: 12px; 
            color: #666; 
            margin-top: 5px; 
            line-height: 1.4;
        }
        .alert {
            padding: 15px;
            margin-bottom: 20px;
            border: 1px solid transparent;
            border-radius: 4px;
        }
        .alert-danger {
            color: #721c24;
            background-color: #f8d7da;
            border-color: #f5c6cb;
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>お問い合わせフォーム</h2>
        
        {if isset($errors.db)}
            <div class="alert alert-danger">{$errors.db}</div>
        {/if}
        
        <form method="post" enctype="multipart/form-data" action="{site_url('contact')}">
            <div class="form-group">
                <label for="name" class="required">お名前</label>
                <input type="text" id="name" name="name" value="{$form_data.name|default:''|escape}" placeholder="山田太郎">
                {if isset($errors.name)}
                    <span class="error">{$errors.name}</span>
                {/if}
            </div>
            
            <div class="form-group">
                <label for="email" class="required">メールアドレス</label>
                <input type="email" id="email" name="email" value="{$form_data.email|default:''|escape}" placeholder="example@example.com">
                {if isset($errors.email)}
                    <span class="error">{$errors.email}</span>
                {/if}
            </div>
            
            <div class="form-group">
                <label for="message" class="required">お問い合わせ内容</label>
                <textarea id="message" name="message" rows="6" placeholder="お問い合わせ内容をご記入ください">{$form_data.message|default:''|escape}</textarea>
                {if isset($errors.message)}
                    <span class="error">{$errors.message}</span>
                {/if}
            </div>
            
            <div class="form-group">
                <label for="upload_file">ファイル添付（任意）</label>
                <input type="file" id="upload_file" name="upload_file" accept=".jpg,.jpeg,.png,.gif,.pdf,.doc,.docx,.txt">
                <div class="file-info">
                    ・最大ファイルサイズ: 5MB<br>
                    ・対応形式: JPG, PNG, GIF, PDF, DOC, DOCX, TXT<br>
                    ・画像の場合は最大2048×2048ピクセル
                </div>
                {if isset($errors.upload_file)}
                    <span class="error">{$errors.upload_file}</span>
                {/if}
            </div>
            
            <div class="form-group">
                <input type="hidden" name="action" value="confirm">
                <button type="submit" class="btn">確認画面へ</button>
            </div>
        </form>
    </div>
</body>
</html>

<!-- 
==============================================
8. Smarty Template: application/views/templates/contact/confirm.tpl
============================================== 
-->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>入力内容確認</title>
    <style>
        * { box-sizing: border-box; }
        body { 
            font-family: 'Hiragino Sans', 'Yu Gothic', Arial, sans-serif; 
            margin: 0; 
            padding: 20px; 
            background-color: #f5f5f5;
        }
        .container { 
            max-width: 600px; 
            margin: 0 auto; 
            background: white; 
            padding: 30px; 
            border-radius: 8px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        h2 { 
            color: #333; 
            margin-bottom: 30px; 
            text-align: center;
            border-bottom: 2px solid #28a745;
            padding-bottom: 10px;
        }
        .confirm-box { 
            background: #f8f9fa; 
            padding: 20px; 
            border-radius: 8px; 
            margin-bottom: 30px;
            border: 1px solid #dee2e6;
        }
        .confirm-item { 
            margin-bottom: 20px; 
            padding-bottom: 15px;
            border-bottom: 1px solid #dee2e6;
        }
        .confirm-item:last-child {
            margin-bottom: 0;
            padding-bottom: 0;
            border-bottom: none;
        }
        .confirm-label { 
            font-weight: bold; 
            margin-bottom: 8px; 
            color: #495057;
            font-size: 14px;
        }
        .confirm-value { 
            padding: 10px; 
            background: white; 
            border-radius: 4px;
            border: 1px solid #ced4da;
            min-height: 40px;
            line-height: 1.5;
        }
        .button-group {
            display: flex;
            gap: 15px;
        }
        .btn { 
            padding: 12px 30px; 
            border: none; 
            border-radius: 4px; 
            cursor: pointer;
            font-size: 16px;
            transition: all 0.3s;
            flex: 1;
        }
        .btn-primary { 
            background: #007bff; 
            color: white; 
        }
        .btn-primary:hover { 
            background: #0056b3; 
        }
        .btn-secondary { 
            background: #6c757d; 
            color: white; 
        }
        .btn-secondary:hover { 
            background: #545b62; 
        }
    </style>
</head>
<body>
    <div class="container">
        <h2>入力内容確認</h2>
        
        <div class="confirm-box">
            <div class="confirm-item">
                <div class="confirm-label">お名前</div>
                <div class="confirm-value">{$form_data.name|escape}</div>
            </div>
            
            <div class="confirm-item">
                <div class="confirm-label">メールアドレス</div>
                <div class="confirm-value">{$form_data.email|escape}</div>
            </div>
            
            <div class="confirm-item">
                <div class="confirm-label">お問い合わせ内容</div>
                <div class="confirm-value">{$form_data.message|nl2br|escape}</div>
            </div>
            
            <div class="confirm-item">
                <div class="confirm-label">添付ファイル</div>
                <div class="confirm-value">
                    {if $uploaded_file}
                        📎 {$original_name|escape}
                    {else}
                        なし
                    {/if}
                </div>
            </div>
        </div>
        
        <form method="post" action="{site_url('contact')}">
            <input type="hidden" name="name" value="{$form_data.name|escape}">
            <input type="hidden" name="email" value="{$form_data.email|escape}">
            <input type="hidden" name="message" value="{$form_data.message|escape}">
            <input type="hidden" name="uploaded_filename" value="{$uploaded_file|escape}">
            <input type="hidden" name="action" value="submit">
            
            <div class="button-group">
                <button type="submit" class="btn btn-primary">送信する</button>
                <button type="button" class="btn btn-secondary" onclick="history.back()">戻って修正</button>
            </div>
        </form>
    </div>
</body>
</html>

<!-- 
==============================================
9. Smarty Template: application/views/templates/contact/complete.tpl
============================================== 
-->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>送信完了</title>
    <style>
        * { box-sizing: border-box; }
        body { 
            font-family: 'Hiragino Sans', 'Yu Gothic', Arial, sans-serif; 
            margin: 0; 
            padding: 20px; 
            background-color: #f5f5f5;
            text-align: center;
        }
        .container { 
            max-width: 500px; 
            margin: 50px auto; 
            background: white; 
            padding: 40px; 
            border-radius: 8px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1);
        }
        .success-icon {
            font-size: 48px;
            color: #28a745;
            margin-bottom: 20px;
        }
        h2 { 
            color: #28a745; 
            margin-bottom: 20px;
        }
        .success-message {
            background: #d4edda; 
            color: #155724; 
            padding: 20px; 
            border-radius: 8px; 
            margin: 20px 0;
            border: 1px solid #c3e6cb;
        }
        .btn { 
            padding: 12px 30px; 
            background: #007bff; 
            color: white; 
            border: none; 
            border-radius: 4px; 
            text-decoration: none; 
            display: inline-block;
            font-size: 16px;
            transition: background-color 0.3s;
            margin-top: 20px;
        }
        .btn:hover { 
            background: #0056b3; 
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="success-icon">✓</div>
        <h2>送信完了</h2>
        
        <div class="success-message">
            <p><strong>お問い合わせを受け付けました。</strong></p>
            <p>ご連絡いただきありがとうございます。<br>
            内容を確認の上、担当者よりご連絡させていただきます。</p>
        </div>
        
        <a href="{site_url('contact')}" class="btn">新規お問い合わせ</a>
    </div>
</body>
</html>
```

主要なポイント
🏗️ 全体構成

MVC構造: Controller（処理制御）→ Model（DB操作）→ View（画面表示）
3画面フロー: 入力 → 確認 → 完了
Smartyテンプレート: PHP処理とHTML表示を分離

🔐 セキュリティ対策

直接アクセス防止: defined('BASEPATH')でチェック
HTMLエスケープ: |escapeでXSS対策
ファイルアップロード制限: 形式・サイズ・権限設定
バリデーション: 必須チェック・文字数制限・メール形式

💾 データ処理フロー

入力画面: フォーム表示とリアルタイム検証
確認処理: バリデーション + ファイルアップロード
送信処理: データベース保存 + セッションクリア
完了画面: 成功メッセージ表示