(覚書)メールフォーム確認画面まで

```php
<?php
/*
==============================================
ファイルアップロード機能（確認画面まで）
==============================================
*/

// Controller部分（Contact.php）
class Contact extends CI_Controller {
    
    public function __construct() {
        parent::__construct();
        $this->load->database();
        $this->load->helper(['form', 'url', 'file']);
        $this->load->library(['form_validation', 'upload', 'session', 'smarty']);
    }
    
    /**
     * メイン処理
     */
    public function index() {
        $data = [
            'errors' => [],
            'form_data' => [],
            'success' => false
        ];
        
        if ($this->input->method() === 'post') {
            $action = $this->input->post('action');
            
            if ($action === 'confirm') {
                $data = $this->_process_confirm();
                if (empty($data['errors'])) {
                    $this->_show_confirm($data);
                    return;
                }
            }
        }
        
        $this->_show_form($data);
    }
    
    /**
     * 確認処理（ファイル名表示対応）
     */
    private function _process_confirm() {
        $data = [
            'errors' => [],
            'form_data' => [],
            'uploaded_files' => [],
            'file_info' => []
        ];
        
        // バリデーション設定
        $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]');
        
        // バリデーション実行
        if (!$this->form_validation->run()) {
            $data['errors'] = $this->form_validation->error_array();
        }
        
        // フォームデータ取得
        $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_temp();
        
        if (!empty($upload_result['errors'])) {
            $data['errors'] = array_merge($data['errors'], $upload_result['errors']);
        } else {
            $data['uploaded_files'] = $upload_result['files'];
            $data['file_info'] = $upload_result['file_info'];
            
            // セッションに保存（確認画面→送信で使用）
            $this->session->set_userdata('temp_uploaded_files', $upload_result['files']);
            $this->session->set_userdata('temp_file_info', $upload_result['file_info']);
        }
        
        return $data;
    }
    
    /**
     * ファイルアップロード処理（一時保存）
     * 投稿者名+input名でファイル名を生成
     */
    private function _handle_file_upload_temp() {
        $result = [
            'files' => [],
            'file_info' => [],
            'errors' => []
        ];
        
        // 投稿者名取得（ファイル名用）
        $user_name = trim($this->input->post('name'));
        $safe_user_name = $this->_sanitize_filename($user_name);
        
        // 複数のファイル入力フィールドに対応
        $file_inputs = [
            'application_form' => '申請書',
            'resume' => '履歴書', 
            'portfolio' => 'ポートフォリオ',
            'certificate' => '証明書',
            'other_document' => 'その他資料'
        ];
        
        // 単一ファイルの場合（従来互換）
        if (isset($_FILES['upload_file']) && !empty($_FILES['upload_file']['name'])) {
            $file_inputs['upload_file'] = '添付ファイル';
        }
        
        foreach ($file_inputs as $input_name => $display_name) {
            if (!isset($_FILES[$input_name]) || empty($_FILES[$input_name]['name'])) {
                continue; // ファイルが選択されていない場合はスキップ
            }
            
            // 一時保存用フォルダ
            $temp_path = './uploads/temp/';
            if (!is_dir($temp_path)) {
                mkdir($temp_path, 0755, true);
            }
            
            // ファイル情報を保存
            $original_name = $_FILES[$input_name]['name'];
            $file_size = $_FILES[$input_name]['size'];
            $file_type = $_FILES[$input_name]['type'];
            $extension = pathinfo($original_name, PATHINFO_EXTENSION);
            
            // 投稿者名+input名でファイル名生成
            // 例：田中太郎_申請書_20241209143022.xlsx
            $timestamp = date('YmdHis');
            $safe_filename = "{$safe_user_name}_{$display_name}_{$timestamp}." . strtolower($extension);
            
            // アップロード設定（エクセルファイル対応）
            $config = [
                'upload_path' => $temp_path,
                'allowed_types' => 'jpg|jpeg|png|gif|pdf|doc|docx|xls|xlsx|txt|zip|csv',
                'max_size' => 10240, // 10MB（エクセルファイルは大きくなりがち）
                'max_width' => 3000,
                'max_height' => 3000,
                'file_name' => $safe_filename,  // カスタムファイル名
                'remove_spaces' => TRUE
            ];
            
            $this->upload->initialize($config);
            
            if ($this->upload->do_upload($input_name)) {
                $upload_data = $this->upload->data();
                
                // アップロードされたファイル情報を保存
                $result['files'][] = $upload_data['file_name'];
                $result['file_info'][] = [
                    'original_name' => $original_name,
                    'saved_name' => $upload_data['file_name'],
                    'input_name' => $input_name,
                    'input_display_name' => $display_name,
                    'file_size' => $file_size,
                    'file_type' => $file_type,
                    'extension' => $extension,
                    'upload_time' => date('Y-m-d H:i:s'),
                    'user_name' => $user_name
                ];
                
            } else {
                $result['errors'][$input_name] = strip_tags($this->upload->display_errors());
            }
        }
        
        return $result;
    }
    
    /**
     * ファイル名用の文字列サニタイズ
     */
    private function _sanitize_filename($name) {
        // 日本語をローマ字に変換（簡易版）
        $name = trim($name);
        
        // 特殊文字を除去
        $name = preg_replace('/[^\p{L}\p{N}\s]/u', '', $name);
        
        // スペースをアンダースコアに変換
        $name = preg_replace('/\s+/', '_', $name);
        
        // 長すぎる場合は切り詰め
        if (mb_strlen($name) > 20) {
            $name = mb_substr($name, 0, 20);
        }
        
        // 空の場合はデフォルト名
        if (empty($name)) {
            $name = 'user_' . date('mdHis');
        }
        
        return $name;
    }
    
    /**
     * フォーム表示
     */
    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_files', $data['uploaded_files']);
        $this->smarty->assign('file_info', $data['file_info']);
        $this->smarty->display('contact/confirm.tpl');
    }
}
?>

<!-- 
==============================================
フォームテンプレート: contact/form.tpl
============================================== 
-->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>お問い合わせフォーム</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
        .container { max-width: 800px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; }
        .form-group { margin-bottom: 20px; }
        .error { color: red; font-size: 14px; margin-top: 5px; }
        label { display: block; margin-bottom: 8px; font-weight: bold; color: #555; }
        input[type="text"], input[type="email"], textarea { 
            width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 4px; font-size: 14px; 
        }
        textarea { resize: vertical; min-height: 100px; }
        .file-input-group {
            border: 1px solid #ddd; border-radius: 4px; padding: 15px; margin-bottom: 15px; background: #fafafa;
        }
        .file-input-title {
            font-weight: bold; color: #495057; margin-bottom: 10px; 
            border-bottom: 1px solid #dee2e6; padding-bottom: 5px;
        }
        .file-info-text { font-size: 12px; color: #666; margin-top: 5px; line-height: 1.4; }
        .file-preview { 
            background: #f5f5f5; padding: 10px; margin-top: 10px; border-radius: 4px; display: none;
        }
        .selected-file {
            margin: 5px 0; padding: 10px; background: white; border: 1px solid #ddd; 
            border-radius: 3px; border-left: 4px solid #007bff;
        }
        .excel-icon::before { content: "📊 "; }
        .pdf-icon::before { content: "📄 "; }
        .doc-icon::before { content: "📝 "; }
        .image-icon::before { content: "🖼️ "; }
        .btn { 
            background: #007bff; color: white; padding: 12px 30px; border: none; 
            border-radius: 4px; font-size: 16px; cursor: pointer; 
        }
        .btn:hover { background: #0056b3; }
    </style>
</head>
<body>
    <div class="container">
        <h2>お問い合わせフォーム</h2>
        
        <form method="post" enctype="multipart/form-data" action="{site_url('contact')}">
            
            <!-- 名前 -->
            <div class="form-group">
                <label for="name">お名前 *</label>
                <input type="text" id="name" name="name" value="{$form_data.name|default:''|escape}" placeholder="山田太郎">
                <div class="file-info-text">※ファイル名に使用されます</div>
                {if isset($errors.name)}
                    <div class="error">{$errors.name}</div>
                {/if}
            </div>
            
            <!-- メールアドレス -->
            <div class="form-group">
                <label for="email">メールアドレス *</label>
                <input type="email" id="email" name="email" value="{$form_data.email|default:''|escape}" placeholder="example@example.com">
                {if isset($errors.email)}
                    <div class="error">{$errors.email}</div>
                {/if}
            </div>
            
            <!-- メッセージ -->
            <div class="form-group">
                <label for="message">お問い合わせ内容 *</label>
                <textarea id="message" name="message" rows="6" placeholder="お問い合わせ内容をご記入ください">{$form_data.message|default:''|escape}</textarea>
                {if isset($errors.message)}
                    <div class="error">{$errors.message}</div>
                {/if}
            </div>
            
            <!-- ファイルアップロード -->
            <div class="form-group">
                <h3>ファイル添付（任意）</h3>
                
                <!-- 申請書 -->
                <div class="file-input-group">
                    <div class="file-input-title excel-icon">申請書（Excelファイル推奨）</div>
                    <input type="file" id="application_form" name="application_form" 
                           accept=".xlsx,.xls,.pdf,.doc,.docx"
                           onchange="showSelectedFile(this, 'application_form_preview')">
                    <div class="file-info-text">保存名例: 山田太郎_申請書_20241209143022.xlsx</div>
                    <div id="application_form_preview" class="file-preview"></div>
                    {if isset($errors.application_form)}
                        <div class="error">{$errors.application_form}</div>
                    {/if}
                </div>
                
                <!-- 履歴書 -->
                <div class="file-input-group">
                    <div class="file-input-title doc-icon">履歴書</div>
                    <input type="file" id="resume" name="resume" 
                           accept=".pdf,.doc,.docx,.xlsx,.xls"
                           onchange="showSelectedFile(this, 'resume_preview')">
                    <div class="file-info-text">保存名例: 山田太郎_履歴書_20241209143022.pdf</div>
                    <div id="resume_preview" class="file-preview"></div>
                    {if isset($errors.resume)}
                        <div class="error">{$errors.resume}</div>
                    {/if}
                </div>
                
                <!-- ポートフォリオ -->
                <div class="file-input-group">
                    <div class="file-input-title image-icon">ポートフォリオ</div>
                    <input type="file" id="portfolio" name="portfolio" 
                           accept=".pdf,.jpg,.jpeg,.png,.zip,.xlsx,.xls"
                           onchange="showSelectedFile(this, 'portfolio_preview')">
                    <div class="file-info-text">保存名例: 山田太郎_ポートフォリオ_20241209143022.pdf</div>
                    <div id="portfolio_preview" class="file-preview"></div>
                    {if isset($errors.portfolio)}
                        <div class="error">{$errors.portfolio}</div>
                    {/if}
                </div>
                
                <!-- 証明書 -->
                <div class="file-input-group">
                    <div class="file-input-title pdf-icon">証明書・資格証</div>
                    <input type="file" id="certificate" name="certificate" 
                           accept=".pdf,.jpg,.jpeg,.png"
                           onchange="showSelectedFile(this, 'certificate_preview')">
                    <div class="file-info-text">保存名例: 山田太郎_証明書_20241209143022.pdf</div>
                    <div id="certificate_preview" class="file-preview"></div>
                    {if isset($errors.certificate)}
                        <div class="error">{$errors.certificate}</div>
                    {/if}
                </div>
                
                <!-- その他資料 -->
                <div class="file-input-group">
                    <div class="file-input-title">その他資料</div>
                    <input type="file" id="other_document" name="other_document" 
                           accept=".pdf,.doc,.docx,.xlsx,.xls,.jpg,.jpeg,.png,.zip,.txt"
                           onchange="showSelectedFile(this, 'other_document_preview')">
                    <div class="file-info-text">保存名例: 山田太郎_その他資料_20241209143022.zip</div>
                    <div id="other_document_preview" class="file-preview"></div>
                    {if isset($errors.other_document)}
                        <div class="error">{$errors.other_document}</div>
                    {/if}
                </div>
                
                <!-- アップロード仕様説明 -->
                <div style="background: #e7f3ff; padding: 15px; border-radius: 4px; margin-top: 15px;">
                    <h4 style="margin-top: 0;">📋 ファイルアップロード仕様</h4>
                    <ul style="margin: 0; padding-left: 20px;">
                        <li><strong>最大ファイルサイズ:</strong> 10MB</li>
                        <li><strong>対応形式:</strong> Excel(.xlsx/.xls), PDF, Word(.doc/.docx), 画像(JPG/PNG), ZIP, CSV, TXT</li>
                        <li><strong>ファイル名:</strong> 自動的に「お名前_種類_日時」の形式で保存されます</li>
                        <li><strong>注意:</strong> Excelファイルは互換性のため.xlsx形式を推奨します</li>
                    </ul>
                </div>
            </div>
            
            <!-- 送信ボタン -->
            <div class="form-group">
                <input type="hidden" name="action" value="confirm">
                <button type="submit" class="btn">確認画面へ</button>
            </div>
        </form>
    </div>
    
    <script>
        // 選択されたファイル名を表示する関数
        function showSelectedFile(input, previewId) {
            const preview = document.getElementById(previewId);
            
            if (input.files && input.files.length > 0) {
                const file = input.files[0];
                const userName = document.getElementById('name').value || 'ユーザー名未入力';
                const inputDisplayName = getInputDisplayName(input.name);
                
                // 予想されるファイル名を生成
                const extension = file.name.split('.').pop();
                const timestamp = new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14);
                const predictedName = `${userName}_${inputDisplayName}_${timestamp}.${extension}`;
                
                // ファイル情報を表示
                preview.innerHTML = `
                    <div class="selected-file">
                        <strong>📎 選択されたファイル:</strong> ${file.name}<br>
                        <strong>💾 保存予定名:</strong> ${predictedName}<br>
                        <strong>📏 サイズ:</strong> ${formatFileSize(file.size)}<br>
                        <strong>📋 種類:</strong> ${file.type || '不明'} (${getFileIcon(extension)} ${extension.toUpperCase()})
                    </div>
                `;
                
                preview.style.display = 'block';
            } else {
                preview.style.display = 'none';
            }
        }
        
        // input名から表示名を取得
        function getInputDisplayName(inputName) {
            const names = {
                'application_form': '申請書',
                'resume': '履歴書',
                'portfolio': 'ポートフォリオ',
                'certificate': '証明書',
                'other_document': 'その他資料'
            };
            return names[inputName] || '添付ファイル';
        }
        
        // ファイル拡張子からアイコンを取得
        function getFileIcon(extension) {
            const ext = extension.toLowerCase();
            if (['xlsx', 'xls', 'csv'].includes(ext)) return '📊';
            if (['pdf'].includes(ext)) return '📄';
            if (['doc', 'docx'].includes(ext)) return '📝';
            if (['jpg', 'jpeg', 'png', 'gif'].includes(ext)) return '🖼️';
            if (['zip', 'rar'].includes(ext)) return '📦';
            return '📄';
        }
        
        // ファイルサイズを読みやすい形式に変換
        function formatFileSize(bytes) {
            if (bytes === 0) return '0 Bytes';
            const k = 1024;
            const sizes = ['Bytes', 'KB', 'MB', 'GB'];
            const i = Math.floor(Math.log(bytes) / Math.log(k));
            return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
        }
        
        // 名前が変更されたときにファイル名プレビューを更新
        document.getElementById('name').addEventListener('input', function() {
            const fileInputs = document.querySelectorAll('input[type="file"]');
            fileInputs.forEach(input => {
                if (input.files && input.files.length > 0) {
                    const previewId = input.id + '_preview';
                    showSelectedFile(input, previewId);
                }
            });
        });
    </script>
</body>
</html>

<!-- 
==============================================
確認画面テンプレート: contact/confirm.tpl
============================================== 
-->
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>入力内容確認</title>
    <style>
        body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
        .container { max-width: 600px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; }
        .confirm-box { background: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; }
        .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; }
        .file-list { margin-top: 10px; }
        .file-item { 
            background: #e7f3ff; padding: 10px; margin: 5px 0; border-radius: 4px; 
            border-left: 4px solid #007bff;
        }
        .btn { padding: 12px 30px; border: none; border-radius: 4px; cursor: pointer; font-size: 16px; text-decoration: none; display: inline-block; margin: 5px; }
        .btn-primary { background: #007bff; color: white; }
        .btn-secondary { background: #6c757d; color: white; }
        .btn:hover { opacity: 0.8; }
    </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 $file_info && count($file_info) > 0}
                        <div class="file-list">
                            {foreach $file_info as $file}
                                <div class="file-item">
                                    <div style="display: flex; align-items: center; gap: 10px;">
                                        <span class="file-icon">
                                            {if $file.extension == 'xlsx' || $file.extension == 'xls'}📊
                                            {elseif $file.extension == 'pdf'}📄
                                            {elseif $file.extension == 'doc' || $file.extension == 'docx'}📝
                                            {elseif $file.extension == 'jpg' || $file.extension == 'jpeg' || $file.extension == 'png'}🖼️
                                            {else}📄{/if}
                                        </span>
                                        <div style="flex: 1;">
                                            <strong>{$file.input_display_name}</strong><br>
                                            <div style="font-size: 14px; color: #666;">
                                                元ファイル名: {$file.original_name|escape}<br>
                                                保存ファイル名: {$file.saved_name|escape}
                                            </div>
                                        </div>
                                        <div style="text-align: right; font-size: 12px; color: #999;">
                                            {$file.file_size|number_format} bytes<br>
                                            {$file.upload_time}
                                        </div>
                                    </div>
                                </div>
                            {/foreach}
                            
                            <div style="margin-top: 15px; padding: 10px; background: #e7f3ff; border-radius: 4px;">
                                <strong>📦 ZIPファイル保存先:</strong><br>
                                <code>uploads/{$smarty.now|date_format:'Y/m/d'}/{$form_data.name|escape}様_{$smarty.now|date_format:'YmdHis'}_xxxxxx/</code>
                            </div>
                        </div>
                    {else}
                        <div style="color: #6c757d; font-style: italic;">ファイルの添付はありません</div>
                    {/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="action" value="submit">
            
            <div style="text-align: center;">
                <button type="submit" class="btn btn-primary">送信する</button>
                <button type="button" class="btn btn-secondary" onclick="history.back()">戻って修正</button>
            </div>
        </form>
    </div>
</body>
</html>
```